├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── __tests__ ├── __mocks__ │ ├── fileMock.js │ ├── jsdomMock.js │ └── styleMock.js ├── setup.js └── tests │ └── index.test.js ├── assetsImg └── screenshot.png ├── example ├── README.md ├── demo.jpg ├── dist │ ├── build.js │ ├── demo.jpg │ ├── index.html │ └── watermark.png ├── example.css ├── example.js ├── example.less ├── index.html ├── watermark.png └── webpack.config.js ├── index.d.ts ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.json ├── src └── index.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "es2015", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "react", 10 | "stage-1" 11 | ], 12 | "plugins": [ 13 | "transform-es2015-modules-commonjs", 14 | "transform-object-rest-spread", 15 | "transform-class-properties", 16 | [ 17 | "transform-runtime", 18 | { 19 | "helpers": false, 20 | "polyfill": false, 21 | "regenerator": true, 22 | "moduleName": "babel-runtime" 23 | } 24 | ] 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parserOptions": { 3 | "ecmaVersion": 8, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "jsx": true 7 | } 8 | }, 9 | "parser": "babel-eslint", 10 | "plugins": [ 11 | "babel", 12 | "react" 13 | ], 14 | "extends": "eslint:recommended", 15 | "env": { 16 | "es6": true, 17 | "browser": true, 18 | "commonjs": true, 19 | }, 20 | "globals": { 21 | }, 22 | "rules": { 23 | "object-shorthand": "error", 24 | "generator-star-spacing": ["error", "after"], 25 | "camelcase": ["error", {"properties": "never"}], 26 | "eqeqeq": ["error", "smart"], 27 | "linebreak-style": ["error", "unix"], 28 | "new-cap": "error", 29 | "no-array-constructor": "error", 30 | "no-lonely-if": "error", 31 | "no-loop-func": "error", 32 | "no-param-reassign": "error", 33 | "no-sequences": "error", 34 | "no-shadow-restricted-names": "error", 35 | "no-unneeded-ternary": "error", 36 | // "no-unused-expressions": "error", 37 | "no-unused-vars": ["error", {"args": "none"}], 38 | "no-use-before-define": ["error", "nofunc"], 39 | "no-var": "error", 40 | "prefer-arrow-callback": "error", 41 | "prefer-spread": "error", 42 | "prefer-template": "error", 43 | "wrap-iife": ["error", "inside"], 44 | "yoda": ["error", "never"], 45 | "react/jsx-uses-react": "error", 46 | "react/jsx-uses-vars": "error", 47 | "react/jsx-no-undef": ["error", {"allowGlobals": true}], 48 | "react/jsx-no-bind": ["error", {"allowArrowFunctions": true}], 49 | "react/jsx-key": "error", 50 | "react/no-unknown-property": "error", 51 | "react/no-string-refs": "error", 52 | "react/no-direct-mutation-state": "error", 53 | } 54 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | lib 3 | assets 4 | npm-debug.log 5 | .DS_Store 6 | .history 7 | npm-error.log 8 | yarn-error.log 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | yarn-error.log 4 | .DS_Store 5 | dist 6 | example 7 | src 8 | .history 9 | assetsImg 10 | index.html 11 | __tests__ 12 | /.* 13 | postcss.config.json 14 | watermark.png 15 | demo.jpg 16 | yarn.lock 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2020 jinke.Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-image-process 2 | 3 | [![npm](https://img.shields.io/npm/dm/react-image-process.svg?style=flat-square)](https://www.npmjs.com/package/react-image-process) 4 | [![npm version](https://img.shields.io/npm/v/react-image-process.svg?style=flat-square)](https://badge.fury.io/js/react-image-process) 5 | [![jest](https://facebook.github.io/jest/img/jest-badge.svg)](https://github.com/facebook/jest) 6 | 7 | > :art: A image process component for react, like compressed image,clip image, add watermarking of image 8 | 9 | [normal version](https://github.com/lijinke666/photo-magician) 10 | 11 | ## Installation 12 | 13 | using `yarn` : 14 | 15 | ``` 16 | yarn add react-image-process 17 | ``` 18 | 19 | using `npm` : 20 | 21 | ``` 22 | npm install react-image-process --save 23 | ``` 24 | 25 | ## Screenshots 26 | 27 | ![lightTheme](https://github.com/lijinke666/react-image-process/blob/master/assetsImg/screenshot.png) 28 | 29 | ## Example 30 | 31 | online example : [https://lijinke666.github.io/react-image-process/](https://lijinke666.github.io/react-image-process/) 32 | 33 | [Source Code](https://github.com/lijinke666/react-image-process/blob/master/example/example.js) 34 | 35 | ## Usage 36 | 37 | ```jsx 38 | import React from 'react'; 39 | import ReactDOM from 'react-dom'; 40 | import ReactImageProcess from 'react-image-process'; 41 | 42 | const onComplete = data => { 43 | console.log('data:', data); 44 | }; 45 | 46 | ReactDOM.render( 47 | 48 | 49 | , 50 | document.getElementById('root') 51 | ); 52 | ``` 53 | 54 | Support multiple Images 55 | 56 | ```jsx 57 | 58 | 59 | 60 | 61 | ``` 62 | 63 | > rotate 64 | 65 | ```jsx 66 | 67 | 68 | 69 | ``` 70 | 71 | > get primary color 72 | 73 | ```jsx 74 | console.log(color)}> 75 | 76 | 77 | ``` 78 | 79 | > waterMark 80 | 81 | ```jsx 82 | 91 | 92 | 93 | ``` 94 | 95 | ```jsx 96 | 105 | 106 | 107 | ``` 108 | 109 | > imageFilter 110 | 111 | ```jsx 112 | 113 | 114 | 115 | ``` 116 | 117 | ## API 118 | 119 | | Property | Description | Type | Default | 120 | | ------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ---------------------- | 121 | | mode | can be set to `base64` `clip` `compress` `rotate` `waterMark` `filter` `primaryColor` | `string` | `base64` | 122 | | onComplete | The callback after trans complete conversion | function(base64Data){} | `-` | 123 | | outputType | image data output type of `blob` | `dataUrl` | `string` | `dataUrl` | 124 | | scale | When the mode is equal to 'clip', the zoom scale of the image. | `number` | `1.0` | 125 | | coordinate | When the mode is equal to 'clip', coordinate of the image. like `[[x1,y1],[x2,y2]]`, if mode equal to `waterMark` like `[x1,y1]` | `number[]` | `-` | 126 | | quality | When the mode is equal to 'compress', quality of the image. | `number` | `0.92` | 127 | | rotate | When the mode is equal to 'rotate', rotate deg of the image. | `number` | `-` | 128 | | waterMark | When the mode is equal to 'waterMark', can be set to `image` or `text` | `string|ReactNode` | `-` | 129 | | waterMarkType | When the mode is equal to 'waterMark', can be set to `image` or `text` | `string` | `text` | 130 | | fontBold | When the mode is equal to 'waterMark' and waterMark equal to `text` ,the font is bold. | `boolean` | `false` | 131 | | fontSize | When the mode is equal to 'waterMark' and waterMark equal to `text` ,the font size | `number` | `20` | 132 | | fontColor | When the mode is equal to 'waterMark' and waterMark equal to `text` ,the font color | `string` | `rgba(255,255,255,.5)` | 133 | | width | When the mode is equal to 'waterMark' and waterMark equal to `image` ,the water width | `number` | `50` | 134 | | height | When the mode is equal to 'waterMark' and waterMark equal to `image` ,the water height | `number` | `50` | 135 | | opacity | When the mode is equal to 'waterMark' and waterMark equal to `image` ,the water opacity range [0-1] | `number` | `0.5` | 136 | | filterType | When the mode is equal to 'filter', can be set to `vintage` `blackWhite` `relief` `blur` | `string` | `vintage` | 137 | 138 | ## Development 139 | 140 | ``` 141 | git clone https://github.com/lijinke666/react-image-process.git 142 | npm install 143 | npm start 144 | ``` 145 | 146 | ## Properties 147 | 148 | ```ts 149 | export type ReactImageProcessMode = 150 | | 'base64' 151 | | 'clip' 152 | | 'compress' 153 | | 'rotate' 154 | | 'waterMark' 155 | | 'filter' 156 | | 'primaryColor'; 157 | 158 | export type ReactImageProcessWaterMarkType = 'image' | 'text'; 159 | export type ReactImageProcessFilterType = 160 | | 'vintage' 161 | | 'blackWhite' 162 | | 'relief' 163 | | 'blur'; 164 | export type ReactImageProcessOutputType = 'blob' | 'dataUrl'; 165 | 166 | export interface ReactImageProcessProps { 167 | mode: ReactImageProcessMode; 168 | waterMarkType: ReactImageProcessWaterMarkType; 169 | filterType: ReactImageProcessFilterType; 170 | outputType: ReactImageProcessOutputType; 171 | waterMark: string; 172 | rotate: number; 173 | quality: number; 174 | coordinate: number[]; 175 | width: number; 176 | height: number; 177 | opacity: number; 178 | fontColor: number; 179 | fontSize: number; 180 | fontBold: number; 181 | onComplete: (data: Blob | string) => void; 182 | } 183 | ``` 184 | 185 | ## License 186 | 187 | [MIT](https://github.com/lijinke666/react-image-process/blob/master/LICENSE) 188 | -------------------------------------------------------------------------------- /__tests__/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | export default "test-file-stub"; 2 | -------------------------------------------------------------------------------- /__tests__/__mocks__/jsdomMock.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-console */ 2 | const JSDOMEnvironment = require("jest-environment-jsdom"); 3 | 4 | module.exports = class CustomizedJSDomEnvironment extends JSDOMEnvironment { 5 | constructor(config) { 6 | const _config = Object.assign(config, { 7 | testEnvironmentOptions: { 8 | beforeParse(window) { 9 | window.document.childNodes.length === 0; 10 | window.alert = msg => { 11 | console.log(msg); 12 | }; 13 | window.matchMedia = () => ({ 14 | addListener: () => {}, 15 | removeListener: () => {} 16 | }); 17 | window.scrollTo = () => {}; 18 | } 19 | } 20 | }); 21 | super(_config); 22 | this.global.jsdom = this.dom; 23 | } 24 | 25 | teardown() { 26 | this.global.jsdom = null; 27 | return super.teardown(); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /__tests__/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /__tests__/setup.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-console */ 2 | const Enzyme = require("enzyme"); 3 | const Adapter = require("enzyme-adapter-react-16"); 4 | 5 | const { JSDOM } = require("jsdom"); 6 | 7 | const jsdom = new JSDOM(""); 8 | const { window } = jsdom; 9 | 10 | function copyProps(src, target) { 11 | const props = Object.getOwnPropertyNames(src) 12 | .filter(prop => typeof target[prop] === "undefined") 13 | .reduce( 14 | (result, prop) => ({ 15 | ...result, 16 | [prop]: Object.getOwnPropertyDescriptor(src, prop) 17 | }), 18 | {} 19 | ); 20 | Object.defineProperties(target, props); 21 | } 22 | 23 | global.window = window; 24 | global.document = window.document; 25 | global.navigator = { 26 | userAgent: "node.js" 27 | }; 28 | copyProps(window, global); 29 | 30 | window.alert = msg => { 31 | console.log(msg); 32 | }; 33 | window.matchMedia = () => ({}); 34 | window.scrollTo = () => {}; 35 | 36 | Enzyme.configure({ adapter: new Adapter() }); 37 | -------------------------------------------------------------------------------- /__tests__/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import assert from "power-assert"; 3 | import { shallow, mount } from "enzyme"; 4 | import ReactImageProcess from "../../src"; 5 | import watermark from "../../example/watermark.png" 6 | import img from "../../example/demo.jpg" 7 | 8 | describe("ReactImageProcess", () => { 9 | it("should render a components", () => { 10 | const wrapper = mount( 11 | 12 | ); 13 | assert(wrapper.find(".react-image-process-base64").length === 1); 14 | assert(wrapper.find(".text-class-name").length >= 1); 15 | }); 16 | it("should render mode == base64", () => { 17 | const wrapper = mount(); 18 | assert(wrapper.props().mode === "base64"); 19 | wrapper.setProps({ mode: "clip" }); 20 | assert(wrapper.props().mode === "clip"); 21 | }) 22 | }); 23 | -------------------------------------------------------------------------------- /assetsImg/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lijinke666/react-image-process/abf8db4b81a22cab2a12c2786718ce0029696401/assetsImg/screenshot.png -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # react-image-process 2 | ### a example 3 | 4 | ``` 5 | $ npm run demo :) 6 | ``` 7 | -------------------------------------------------------------------------------- /example/demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lijinke666/react-image-process/abf8db4b81a22cab2a12c2786718ce0029696401/example/demo.jpg -------------------------------------------------------------------------------- /example/dist/demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lijinke666/react-image-process/abf8db4b81a22cab2a12c2786718ce0029696401/example/dist/demo.jpg -------------------------------------------------------------------------------- /example/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | react-image-process 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/dist/watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lijinke666/react-image-process/abf8db4b81a22cab2a12c2786718ce0029696401/example/dist/watermark.png -------------------------------------------------------------------------------- /example/example.css: -------------------------------------------------------------------------------- 1 | #root, 2 | body, 3 | html { 4 | height: 100%; 5 | } 6 | body, 7 | html { 8 | margin: 0; 9 | padding: 0; 10 | } 11 | body { 12 | background: #f0f2f5; 13 | padding: 40px; 14 | } 15 | h1 { 16 | font-weight: 400; 17 | } 18 | h2 { 19 | font-weight: 300; 20 | margin-top: 30px; 21 | } 22 | @media (max-width: 768px) { 23 | h2 { 24 | text-align: center; 25 | } 26 | } 27 | img.example-img { 28 | display: block; 29 | max-width: 100%; 30 | margin: 10px 0; 31 | } 32 | @media (max-width: 768px) { 33 | img.example-img { 34 | margin: 10px auto; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import ReactImageProcess from '../src'; 4 | import swal from 'sweetalert'; 5 | import { name } from '../package.json'; 6 | 7 | import demoImg from './demo.jpg'; 8 | import waterMark from './watermark.png'; 9 | 10 | import './example.less'; 11 | 12 | const onComplete = data => { 13 | console.log('data:', data); 14 | }; 15 | 16 | class Demo extends React.PureComponent { 17 | constructor(props) { 18 | super(props); 19 | } 20 | state = { 21 | primaryColor: 'transparent' 22 | }; 23 | getPrimaryColorComplete = primaryColor => { 24 | console.log('primaryColor:', primaryColor); 25 | this.setState({ primaryColor }); 26 | }; 27 | render() { 28 | const { primaryColor } = this.state; 29 | return ( 30 | 31 |

32 | {name}{' '} 33 | 37 | [Source Code] 38 | 39 |

40 |
41 | 42 |

Base Image

43 | clip 44 | 45 |

base64

46 | 51 | base64 56 | swal({ 57 | text: ` 58 | { 59 | mode:'base64' 60 | } 61 | ` 62 | }) 63 | } 64 | /> 65 | 66 | 67 |

clip

68 | 73 | clip 78 | swal({ 79 | text: ` 80 | { 81 | mode:'clip', 82 | scale:1, 83 | coordinate:[[200, 200], [300, 300]] 84 | } 85 | ` 86 | }) 87 | } 88 | /> 89 | 90 | 95 | clip 100 | swal({ 101 | text: ` 102 | { 103 | mode:'clip', 104 | scale:2, 105 | coordinate:[[200, 200], [600, 600]]} 106 | ` 107 | }) 108 | } 109 | /> 110 | 111 | 112 |

compress

113 | 114 | compress 119 | swal({ 120 | text: ` 121 | { 122 | mode:'compress', 123 | quality:0.1 124 | } 125 | ` 126 | }) 127 | } 128 | /> 129 | 130 | 131 | compress 136 | swal({ 137 | text: ` 138 | { 139 | mode:'compress', 140 | quality:0.01 141 | } 142 | ` 143 | }) 144 | } 145 | /> 146 | 147 | 148 |

rotate

149 | 150 | rotate 155 | swal({ 156 | text: ` 157 | { 158 | mode:'rotate', 159 | rotate:30 160 | }` 161 | }) 162 | } 163 | /> 164 | 165 | 166 |

primaryColor

167 | 171 | {primaryColor} 177 | swal({ 178 | text: ` 179 | { 180 | mode:'primaryColor' 181 | }` 182 | }) 183 | } 184 | /> 185 | 186 |
{primaryColor}
187 | 188 |

waterMark

189 | 198 | waterMark 203 | swal({ 204 | text: ` 205 | { 206 | mode:'waterMark', 207 | waterMarkType:'image' 208 | waterMark={waterMark} 209 | width:60 210 | height:60 211 | opacity:0.8 212 | coordinate:[330, 300] 213 | } 214 | ` 215 | }) 216 | } 217 | /> 218 | 219 | 228 | waterMark 233 | swal({ 234 | text: ` 235 | { 236 | mode:'waterMark', 237 | waterMarkType:'text'. 238 | waterMark={${name}}. 239 | fontBold:false, 240 | fontSize:30 241 | fontColor:"#396" 242 | coordinate:[10,20] 243 | } 244 | ` 245 | }) 246 | } 247 | /> 248 | 249 | 250 |

imageFilter

251 | 252 | vintage 257 | swal({ 258 | text: ` 259 | { 260 | mode:'filter', 261 | filterType:'vintage' 262 | } 263 | ` 264 | }) 265 | } 266 | /> 267 | 268 | 269 | blackWhite 274 | swal({ 275 | text: ` 276 | { 277 | mode:'filter', 278 | filterType:'blackWhite' 279 | } 280 | ` 281 | }) 282 | } 283 | /> 284 | 285 | 286 | relief 291 | swal({ 292 | text: ` 293 | { 294 | mode:'filter', 295 | filterType:'relief' 296 | } 297 | ` 298 | }) 299 | } 300 | /> 301 | 302 | 303 | blur 308 | swal({ 309 | text: ` 310 | { 311 | mode:'filter', 312 | filterType:'blur' 313 | } 314 | ` 315 | }) 316 | } 317 | /> 318 | 319 |
320 | ); 321 | } 322 | } 323 | 324 | ReactDOM.render(, document.getElementById('root')); 325 | -------------------------------------------------------------------------------- /example/example.less: -------------------------------------------------------------------------------- 1 | #root,body,html{ 2 | height:100% 3 | } 4 | body,html{ 5 | margin:0; 6 | padding: 0; 7 | } 8 | body{ 9 | background: #f0f2f5; 10 | padding: 40px 11 | } 12 | h1{ 13 | font-weight: 400; 14 | } 15 | h2{ 16 | font-weight: 300; 17 | margin-top:30px; 18 | @media (max-width:768px) { 19 | text-align: center; 20 | } 21 | } 22 | img.example-img{ 23 | display: block; 24 | max-width:100%; 25 | margin:10px 0; 26 | @media (max-width:768px) { 27 | margin:10px auto; 28 | } 29 | } -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | react-image-process 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lijinke666/react-image-process/abf8db4b81a22cab2a12c2786718ce0029696401/example/watermark.png -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | 5 | const PORT = 8083 6 | 7 | module.exports = env => { 8 | const mode = (env && env.mode) || 'development' 9 | const options = { 10 | mode, 11 | entry: path.join(__dirname, '../example/example.js'), 12 | output: { 13 | path: path.join(__dirname, '../example/dist'), 14 | filename: 'build.js', 15 | publicPath: mode === 'development' ? '' : './example/dist/' 16 | }, 17 | //模块加载器 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.js[x]?$/, 22 | use: [ 23 | { 24 | loader: 'babel-loader' 25 | } 26 | ], 27 | exclude: '/node_modules/' 28 | }, 29 | { 30 | test: /\.less$/, 31 | use: [ 32 | { loader: 'style-loader' }, 33 | { 34 | loader: 'css-loader', 35 | options: { minimize: false, sourceMap: true } 36 | }, 37 | { loader: 'less-loader', options: { sourceMap: true } } 38 | ] 39 | }, 40 | { 41 | test: /\.css$/, 42 | use: [ 43 | { loader: 'style-loader' }, //loader 倒序执行 先执行 less-laoder 44 | { 45 | loader: 'css-loader', 46 | options: { minimize: false, sourceMap: true } 47 | } 48 | ] 49 | }, 50 | { 51 | test: /\.(jpg|jpeg|png|gif|cur|ico)$/, 52 | use: [ 53 | { 54 | loader: 'file-loader', 55 | options: { 56 | name: '[name].[ext]' //遇到图片 生成一个images文件夹 名字.后缀的图片 57 | } 58 | } 59 | ] 60 | }, 61 | { 62 | test: /\.(eot|ttf|svg|woff|woff2)$/, 63 | use: [ 64 | { 65 | loader: 'file-loader', 66 | options: { 67 | name: 'fonts/[name][hash:8].[ext]' 68 | } 69 | } 70 | ] 71 | } 72 | ] 73 | }, 74 | //自动补全后缀 75 | resolve: { 76 | enforceExtension: false, 77 | extensions: ['.js', '.jsx', '.json'], 78 | modules: [path.resolve('src'), path.resolve('.'), 'node_modules'] 79 | }, 80 | devServer: { 81 | contentBase: path.join(__dirname), 82 | inline: true, 83 | port: PORT, 84 | publicPath: '/dist/', 85 | historyApiFallback: true, 86 | stats: { 87 | color: true, 88 | errors: true, 89 | version: true, 90 | warnings: true, 91 | progress: true 92 | } 93 | }, 94 | plugins: [ 95 | new webpack.LoaderOptionsPlugin({ 96 | options: { 97 | chunksSortMode: 'none' 98 | } 99 | }), 100 | new HtmlWebpackPlugin({ 101 | title: 'demo', 102 | filename: 'index.html', 103 | template: path.resolve(__dirname, 'index.html'), //模板文件 104 | hash: true //添加hash码 105 | }) 106 | ] 107 | } 108 | return options 109 | } 110 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export type ReactImageProcessMode = 4 | | 'base64' 5 | | 'clip' 6 | | 'compress' 7 | | 'rotate' 8 | | 'waterMark' 9 | | 'filter' 10 | | 'primaryColor'; 11 | 12 | export type ReactImageProcessWaterMarkType = 'image' | 'text'; 13 | export type ReactImageProcessFilterType = 14 | | 'vintage' 15 | | 'blackWhite' 16 | | 'relief' 17 | | 'blur'; 18 | export type ReactImageProcessOutputType = 'blob' | 'dataUrl'; 19 | 20 | export interface ReactImageProcessProps { 21 | mode?: ReactImageProcessMode; 22 | waterMarkType?: ReactImageProcessWaterMarkType; 23 | filterType?: ReactImageProcessFilterType; 24 | outputType?: ReactImageProcessOutputType; 25 | waterMark?: string; 26 | rotate?: number; 27 | quality?: number; 28 | coordinate?: number[]; 29 | width?: number; 30 | height?: number; 31 | opacity?: number; 32 | fontColor?: number; 33 | fontSize?: number; 34 | fontBold?: number; 35 | onComplete?: (data: Blob | string) => void; 36 | } 37 | 38 | export default class ReactImageProcess extends React.PureComponent< 39 | ReactImageProcessProps, 40 | any 41 | > {} 42 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | react-image-process 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-image-process", 3 | "version": "0.2.4", 4 | "description": "a image process component for react", 5 | "main": "lib/index.js", 6 | "typing": "index.d.ts", 7 | "scripts": { 8 | "start": "yarn demo", 9 | "test": "jest --no-cache __tests__/tests", 10 | "clean": "rimraf lib && rimraf assets", 11 | "auto": "postcss -u autoprefixer -c postcss.config.json --no-map -o assets/index.css assets/index.css", 12 | "build:css": "lessc src/index.less assets/index.css && yarn auto", 13 | "build:js": "babel src -d lib", 14 | "build": "cross-env NODE_ENV=production && yarn run clean && yarn build:js", 15 | "build:demo": "cross-env NODE_ENV=production rimraf example/dist && webpack --env.mode=production --progress --config ./example/webpack.config.js", 16 | "demo": "cross-env NODE_ENV=development && webpack-dev-server --progress --inline --hot --config ./example/webpack.config.js", 17 | "prepare": "yarn build", 18 | "precommit": "lint-staged", 19 | "lint": "prettier --write \"src/**/*.js\" && eslint_d --fix src" 20 | }, 21 | "pre-commit": "lint", 22 | "lint-staged": { 23 | "src/**/*.js": [ 24 | "prettier --write", 25 | "eslint_d --fix", 26 | "git add" 27 | ] 28 | }, 29 | "author": "Jinke.Li <1359518268@qq.com>", 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/lijinke666/react-image-process" 33 | }, 34 | "homepage": "https://lijinke666.github.io/react-image-process", 35 | "bugs": { 36 | "url": "https://github.com/lijinke666/react-image-process/issues" 37 | }, 38 | "license": "MIT", 39 | "keywords": [ 40 | "react", 41 | "reactjs", 42 | "react-photo", 43 | "compress", 44 | "photo", 45 | "react-filter", 46 | "react-image", 47 | "react-image-process", 48 | "image-process", 49 | "photo-magician", 50 | "image-process", 51 | "photo", 52 | "filter", 53 | "clip", 54 | "crop", 55 | "component" 56 | ], 57 | "dependencies": { 58 | "classnames": "^2.2.5", 59 | "photo-magician": "^0.3.0", 60 | "prop-types": "^15.6.0", 61 | "babel-runtime": "^6.23.0" 62 | }, 63 | "devDependencies": { 64 | "autoprefixer": "^6.7.2", 65 | "babel-cli": "^6.16.0", 66 | "babel-core": "6.x", 67 | "babel-eslint": "^8.2.3", 68 | "babel-jest": "^22.4.3", 69 | "babel-loader": "6.x", 70 | "babel-plugin-add-module-exports": "^0.2.1", 71 | "babel-plugin-dynamic-import-node": "^1.0.2", 72 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 73 | "babel-plugin-syntax-object-rest-spread": "^6.13.0", 74 | "babel-plugin-transform-async-to-generator": "^6.24.1", 75 | "babel-plugin-transform-class-properties": "^6.23.0", 76 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 77 | "babel-plugin-transform-object-assign": "^6.22.0", 78 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 79 | "babel-plugin-transform-runtime": "^6.23.0", 80 | "babel-preset-es2015": "^6.18.0", 81 | "babel-preset-react": "^6.16.0", 82 | "babel-preset-stage-0": "6.x", 83 | "babel-preset-stage-1": "^6.24.1", 84 | "cross-env": "^5.1.4", 85 | "css-loader": "~0.28.11", 86 | "enzyme": "^3.3.0", 87 | "enzyme-adapter-react-16": "^1.1.1", 88 | "enzyme-to-json": "^3.3.3", 89 | "eslint": "^4.19.1", 90 | "eslint-plugin-babel": "^5.1.0", 91 | "eslint-plugin-react": "^7.7.0", 92 | "eslint_d": "^5.3.0", 93 | "extract-text-webpack-plugin": "^2.0.0-beta.4", 94 | "file-loader": "^0.9.0", 95 | "html-webpack-plugin": "^3.2.0", 96 | "jest": "^22.4.3", 97 | "jest-environment-jsdom": "^22.4.3", 98 | "jsdom": "^11.9.0", 99 | "less": "^2.7.2", 100 | "less-loader": "^2.2.3", 101 | "lint-staged": "^7.0.5", 102 | "open-browser-webpack-plugin": "0.0.5", 103 | "optimize-css-assets-webpack-plugin": "^1.3.0", 104 | "postcss": "^6.0.12", 105 | "postcss-cli": "^4.1.1", 106 | "postcss-loader": "^1.2.2", 107 | "power-assert": "^1.5.0", 108 | "prettier": "^1.12.1", 109 | "react": "^16.3.2", 110 | "react-dom": "^16.3.2", 111 | "react-hot-loader": "^4.1.2", 112 | "react-loader": "^2.4.0", 113 | "regenerator-runtime": "^0.11.0", 114 | "rimraf": "^2.6.0", 115 | "style-loader": "~0.13.0", 116 | "sweetalert": "^2.1.0", 117 | "url-loader": "^0.5.8", 118 | "webpack": "^4.2.0", 119 | "webpack-cli": "^3.3.5", 120 | "webpack-dev-server": "^3.1.11" 121 | }, 122 | "jest": { 123 | "moduleFileExtensions": [ 124 | "js", 125 | "jsx", 126 | "json" 127 | ], 128 | "transformIgnorePatterns": [ 129 | "/node_modules/" 130 | ], 131 | "modulePathIgnorePatterns": [ 132 | "/.history/" 133 | ], 134 | "moduleDirectories": [ 135 | "node_modules", 136 | ".", 137 | "src", 138 | "src/shared" 139 | ], 140 | "setupTestFrameworkScriptFile": "/__tests__/setup.js", 141 | "snapshotSerializers": [ 142 | "enzyme-to-json/serializer" 143 | ], 144 | "collectCoverageFrom": [ 145 | "src/**/*.{js,jsx}" 146 | ], 147 | "transform": { 148 | "^.+\\.jsx?$": "babel-jest" 149 | }, 150 | "moduleNameMapper": { 151 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__tests__/__mocks__/fileMock.js", 152 | "\\.(css|less)$": "/__tests__/__mocks__/styleMock.js" 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /postcss.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoprefixer": { 3 | "browsers": [ 4 | "last 6 versions", 5 | "Android >= 4.0", 6 | "Firefox ESR", 7 | "not ie < 9" 8 | ], 9 | "sourceMap":false 10 | } 11 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name react-image-process 3 | * @version 0.2.0 4 | */ 5 | 6 | import React, { PureComponent } from 'react' 7 | import PhotoMagician from 'photo-magician' 8 | import PropTypes from 'prop-types' 9 | import classnames from 'classnames' 10 | 11 | export const MODE_TYPE = { 12 | base64: 'base64', 13 | clip: 'clip', 14 | compress: 'compress', 15 | rotate: 'rotate', 16 | waterMark: 'waterMark', 17 | filter: 'filter', 18 | primaryColor: 'primaryColor' 19 | } 20 | 21 | export const WATER_MARK_TYPE = { 22 | image: 'image', 23 | text: 'text' 24 | } 25 | 26 | export const FILTER_TYPE = { 27 | vintage: 'vintage', 28 | blackWhite: 'blackWhite', 29 | relief: 'relief', 30 | blur: 'blur' 31 | } 32 | 33 | export const OUTPUT_TYPE = { 34 | blob: 'blob', 35 | dataUrl: 'dataUrl' 36 | } 37 | 38 | const MODE = Object.values(MODE_TYPE) 39 | const WATER_MARK = Object.values(WATER_MARK_TYPE) 40 | const FILTER = Object.values(FILTER_TYPE) 41 | const OUTPUT = Object.values(OUTPUT_TYPE) 42 | 43 | const mainPrefix = 'react-image-process' 44 | 45 | export default class ReactImageProcess extends PureComponent { 46 | constructor(props) { 47 | super(props) 48 | } 49 | static defaultProps = { 50 | mode: MODE_TYPE['base64'], 51 | waterMarkType: WATER_MARK_TYPE['text'], 52 | filterType: FILTER_TYPE['vintage'], 53 | outputType: OUTPUT_TYPE['dataUrl'], 54 | waterMark: mainPrefix, 55 | rotate: 0, 56 | quality: 1.0, 57 | coordinate: [0, 0], 58 | width: 50, 59 | height: 50, 60 | opacity: 0.5, 61 | fontColor: 'rgba(255,255,255,.5)', 62 | fontSize: 20, 63 | fontBold: true, 64 | onComplete: () => {} 65 | } 66 | static propTypes = { 67 | mode: PropTypes.oneOf(MODE), 68 | waterMarkType: PropTypes.oneOf(WATER_MARK), 69 | filterType: PropTypes.oneOf(FILTER), 70 | outputType: PropTypes.oneOf(OUTPUT), 71 | waterMark: PropTypes.oneOfType([ 72 | PropTypes.string, 73 | PropTypes.element, 74 | PropTypes.node 75 | ]), 76 | scale: PropTypes.number, 77 | rotate: PropTypes.number, 78 | quality: PropTypes.number, 79 | width: PropTypes.number, 80 | height: PropTypes.number, 81 | fontColor: PropTypes.string, 82 | fontSize: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 83 | fontBold: PropTypes.bool, 84 | coordinate: PropTypes.array, 85 | onComplete: PropTypes.func 86 | } 87 | render() { 88 | const { mode, children, style, className } = this.props 89 | 90 | const _className = `${mainPrefix}-${mode}` 91 | 92 | return ( 93 | (this.node = node)} 96 | {...style} 97 | > 98 | {children} 99 | 100 | ) 101 | } 102 | /** 103 | * @description get base64 data of the image 104 | * @param {Object} options 105 | * @param {String | Object} options.cover cover url | image element node The next cover parameter is the same as this. 106 | * @return base64 data 107 | */ 108 | base64Handler = async (cover, params) => { 109 | return await this.photoMagician.toBase64Url({ cover, ...params }) 110 | } 111 | 112 | /** 113 | * @description cut clip of the image 114 | * @param {object} Options 115 | * @param {String | Object} options.cover 116 | * @param {Number} options.scale Image zooming default '1.0' 117 | * @param {Array} options.coordinate [[x1,y1],[x2,y2]] 118 | * @return image node 119 | */ 120 | clipHandler = async (cover, params) => { 121 | return await this.photoMagician.clipImage({ cover, ...params }) 122 | } 123 | /** 124 | * @description compress of the image 125 | * @param {Object} options 126 | * @param {String | Object} options.cover 127 | * @param {Number} options.quality range(0-1) default '0.92' 128 | * @return base64 data 129 | */ 130 | compressHandler = async (cover, params) => { 131 | return await this.photoMagician.compressImage({ cover, ...params }) 132 | } 133 | /** 134 | * @description Rotate the image 135 | * @param {String | Object} cover 图片地址或节点 136 | * @param {Number} rotate 旋转比例 (0 -360 ) ° 137 | */ 138 | rotateHandler = async (cover, params) => { 139 | return await this.photoMagician.rotateImage({ cover, ...params }) 140 | } 141 | /** 142 | * @param {Object} options 143 | * @param {String | Object} options.cover 144 | * @param {String} options.mode filter name "vintage" | "blackWhite" | "relief" | "blur" 145 | */ 146 | filterHandler = async (cover, { filterType, ...params }) => { 147 | return await this.photoMagician.addImageFilter({ 148 | cover, 149 | ...params, 150 | mode: filterType 151 | }) 152 | } 153 | waterMarkHandler = async (cover, { waterMarkType, ...params }) => { 154 | return await this.photoMagician.addWaterMark({ 155 | cover, 156 | ...params, 157 | mode: waterMarkType 158 | }) 159 | } 160 | /** 161 | * @description get primary color of the image 162 | * @param {Object} options 163 | * @param {String | Object} options.cover 164 | * @return primaryColor 165 | */ 166 | primaryColorHandler = async cover => { 167 | return await this.photoMagician.getPrimaryColor({ cover }) 168 | } 169 | baseHandler = async (mode, options) => { 170 | try { 171 | const { onComplete } = this.props 172 | const { children, ...params } = options 173 | const images = Array.isArray(children) 174 | ? [...options.children] 175 | : [options.children] 176 | 177 | for (let [ 178 | i, 179 | { 180 | props: { src } 181 | } 182 | ] of images.entries()) { 183 | if (!src) continue 184 | const data = await this[`${mode}Handler`](src, params) 185 | if (mode !== MODE_TYPE['primaryColor']) { 186 | if (params.outputType === OUTPUT_TYPE.dataUrl) { 187 | return (this.currentImgNodes[i].src = data) 188 | } 189 | this.currentImgNodes[i].src = URL.createObjectURL(data) 190 | this.currentImgNodes[i].onload = () => { 191 | URL.revokeObjectURL(data) 192 | } 193 | } 194 | if (onComplete && onComplete instanceof Function) { 195 | onComplete(data) 196 | } 197 | } 198 | } catch (err) { 199 | /*eslint-disable no-console */ 200 | console.error(`[${mode}Handler-error]:`, err) 201 | } 202 | } 203 | //图片处理 204 | imageHandle = async ({ mode, ...options }) => { 205 | await this.baseHandler(MODE_TYPE[mode], options) 206 | } 207 | componentWillUnmount() { 208 | this.photoMagician = undefined 209 | this.currentImgNodes = undefined 210 | this.node = undefined 211 | } 212 | componentDidMount() { 213 | this.currentImgNodes = this.node.querySelectorAll('img') 214 | this.photoMagician = new PhotoMagician() 215 | this.imageHandle(this.props) 216 | } 217 | 218 | componentWillReceiveProps(nextProps) { 219 | if ( 220 | Object.keys(nextProps).some( 221 | key => !Object.is(nextProps[key], this.props[key]) 222 | ) 223 | ) { 224 | this.imageHandle(nextProps) 225 | } 226 | } 227 | } 228 | --------------------------------------------------------------------------------