1 | const querystring = require('querystring')
|
2 | const { AliImgError } = require('./error')
|
3 |
|
4 |
|
5 | const MAX_BYTE_SIZE = 64
|
6 |
|
7 | const TYPE_LIST = ['jpg', 'png', 'webp', 'bmp', 'gif', 'tiff']
|
8 |
|
9 | const FONTS_LIST = ['wqy-zenhei', 'wqy-microhei', 'fangzhengshusong', 'fangzhengkaiti', 'fangzhengheiti', 'fangzhengfangsong', 'droidsansfallback']
|
10 |
|
11 | const FONTS_LIST_CN = ['文泉驿正黑', '文泉微米黑', '方正书宋', '方正楷体', '方正黑体', '方正仿宋', 'DroidSansFallback']
|
12 |
|
13 | const ORIGIN_LIST = ['nw', 'north', 'ne', 'west', 'center', 'east', 'sw', 'south', 'se']
|
14 |
|
15 | const RESIZE_MODE = ['lfit', 'mfit', 'fill', 'pad', 'fixed']
|
16 |
|
17 | class Img {
|
18 | constructor (path) {
|
19 | this.data = {path, objectName: this.getObjectName()}
|
20 | this.child = [this.data]
|
21 |
|
22 |
|
23 |
|
24 | this.query = {}
|
25 |
|
26 |
|
27 | this.style = {}
|
28 | }
|
29 |
|
30 | |
31 |
|
32 |
|
33 |
|
34 | resize (opt) {
|
35 | if (typeof opt !== 'object') {
|
36 | throw new AliImgError('resize() opt 应为对象')
|
37 | }
|
38 |
|
39 | let param = {}
|
40 |
|
41 | let lowerLimit = 1
|
42 | let upperLimit = 4096
|
43 |
|
44 | if (typeof opt.width === 'number') {
|
45 |
|
46 | param.w = Math.max(Math.min(opt.width, upperLimit), lowerLimit)
|
47 | } else if (opt.width) {
|
48 | throw new AliImgError('resize() width 应为数字类型')
|
49 | }
|
50 |
|
51 |
|
52 | if (typeof opt.height === 'number') {
|
53 |
|
54 | param.h = Math.max(Math.min(opt.height, upperLimit), lowerLimit)
|
55 | } else if (opt.height) {
|
56 | throw new AliImgError('resize() height 应为数字类型')
|
57 | }
|
58 |
|
59 |
|
60 | if (typeof opt.longest === 'number') {
|
61 |
|
62 | param.l = Math.max(Math.min(opt.longest, upperLimit), lowerLimit)
|
63 | } else if (opt.longest) {
|
64 | throw new AliImgError('resize() longest 应为数字类型')
|
65 | }
|
66 |
|
67 |
|
68 | if (typeof opt.shortest === 'number') {
|
69 |
|
70 | param.s = Math.max(Math.min(opt.shortest, upperLimit), lowerLimit)
|
71 | } else if (opt.shortest) {
|
72 | throw new AliImgError('resize() shortest 应为数字类型')
|
73 | }
|
74 |
|
75 |
|
76 | if (typeof opt.limit === 'number') {
|
77 |
|
78 | param.limit = Number(Boolean(opt.limit))
|
79 | } else if (opt.limit) {
|
80 | throw new AliImgError('resize() limit 应为数字类型')
|
81 | }
|
82 |
|
83 |
|
84 | if (RESIZE_MODE.includes(opt.mode)) {
|
85 | param.m = opt.mode
|
86 | } else if (opt.mode) {
|
87 | throw new AliImgError('resize() mode 值不支持')
|
88 | }
|
89 |
|
90 |
|
91 | if (typeof opt.color === 'string') {
|
92 | param.color = opt.color
|
93 | } else if (opt.color) {
|
94 | throw new AliImgError('resize() color 应为字符串')
|
95 | }
|
96 |
|
97 |
|
98 | if (typeof opt.percent === 'number') {
|
99 | let lowerLimit = 1
|
100 | let upperLimit = 1000
|
101 |
|
102 | param.p = Math.max(Math.min(opt.percent, upperLimit), lowerLimit)
|
103 | } else if (opt.percent) {
|
104 | throw new AliImgError('resize() percent 应为数字类型')
|
105 | }
|
106 |
|
107 | this.query.resize = param
|
108 | return this
|
109 | }
|
110 |
|
111 | |
112 |
|
113 |
|
114 |
|
115 | circle (radius) {
|
116 | if (typeof radius !== 'number') {
|
117 | throw new AliImgError('circle() radius 应为数字类型')
|
118 | }
|
119 |
|
120 | this.query.circle = { r: radius }
|
121 | return this
|
122 | }
|
123 |
|
124 | |
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 | crop (x, y, w, h, origin) {
|
134 | let argsMap = ['x', 'y', 'width', 'height']
|
135 | for (let i = 0; i < 4; i++) {
|
136 | if (typeof arguments[i] !== 'number') {
|
137 | throw new AliImgError(`crop() ${argsMap[i]} 应为数字类型`)
|
138 | }
|
139 | }
|
140 |
|
141 |
|
142 | let originOpts = ORIGIN_LIST.concat(undefined)
|
143 | if (!originOpts.includes(origin)) {
|
144 | throw new AliImgError('crop() origin 不支持该值')
|
145 | }
|
146 |
|
147 |
|
148 | let lowerLimit = 0
|
149 | w = Math.max(w, lowerLimit)
|
150 | h = Math.max(h, lowerLimit)
|
151 | x = Math.max(x, lowerLimit)
|
152 | y = Math.max(y, lowerLimit)
|
153 |
|
154 | let param = { x, y, w, h }
|
155 | if (origin) {
|
156 | param.g = origin
|
157 | }
|
158 | this.query.crop = param
|
159 | return this
|
160 | }
|
161 |
|
162 | |
163 |
|
164 |
|
165 |
|
166 | indexCrop (opt) {
|
167 | if (typeof opt !== 'object') {
|
168 | throw new AliImgError('indexCrop() opt 应为配置对象')
|
169 | }
|
170 | if (typeof opt.i !== 'number') {
|
171 | throw new AliImgError('indexCrop() 索引应为数字类型')
|
172 | }
|
173 | if (typeof opt.x !== 'number' && typeof opt.y !== 'number') {
|
174 | throw new AliImgError('indexCrop() x 或 y 应为数字类型')
|
175 | }
|
176 |
|
177 | let newOpt = {...opt}
|
178 |
|
179 | let lowerLimit = 0
|
180 |
|
181 | newOpt.i = Math.max(newOpt.i, lowerLimit)
|
182 |
|
183 | newOpt.i = parseInt(newOpt.i)
|
184 |
|
185 | this.query.indexcrop = newOpt
|
186 | return this
|
187 | }
|
188 |
|
189 | |
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 | roundedCorners (radius) {
|
197 | if (typeof radius !== 'number') {
|
198 | throw new AliImgError('roundedCorners() radius 应为数字类型')
|
199 | }
|
200 |
|
201 | let lowerLimit = 1
|
202 | let upperLimit = 4096
|
203 |
|
204 | radius = Math.max(Math.min(radius, upperLimit), lowerLimit)
|
205 |
|
206 | this.query['rounded-corners'] = { r: radius }
|
207 | return this
|
208 | }
|
209 |
|
210 | |
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 | autoOrient (value) {
|
217 | if (typeof value !== 'number') {
|
218 | throw new AliImgError('autoOrient() value 应为数字类型')
|
219 | }
|
220 |
|
221 |
|
222 | value = Number(Boolean(value))
|
223 | this.query['auto-orient'] = value
|
224 | return this
|
225 | }
|
226 |
|
227 | |
228 |
|
229 |
|
230 |
|
231 | rotate (angle) {
|
232 | if (typeof angle !== 'number') {
|
233 | throw new AliImgError('rotate() angle 应为数字类型')
|
234 | }
|
235 |
|
236 | let origin = this.query.rotate || 0
|
237 |
|
238 |
|
239 | this.query.rotate = ((origin + angle) % 360 + 360) % 360
|
240 | return this
|
241 | }
|
242 |
|
243 | |
244 |
|
245 |
|
246 |
|
247 |
|
248 | blur (radius, standard) {
|
249 | if (typeof radius !== 'number') {
|
250 | throw new AliImgError('blur() radius 应为数字类型')
|
251 | }
|
252 | if (typeof standard !== 'number') {
|
253 | throw new AliImgError('blur() standard 应为数字类型')
|
254 | }
|
255 |
|
256 |
|
257 | radius += this.query.blur ? this.query.blur.r : 0
|
258 | standard += this.query.blur ? this.query.blur.s : 0
|
259 |
|
260 |
|
261 | let lowerLimit = 1
|
262 | let upperLimit = 50
|
263 |
|
264 | radius = Math.max(Math.min(radius, upperLimit), lowerLimit)
|
265 | standard = Math.max(Math.min(standard, upperLimit), lowerLimit)
|
266 |
|
267 | this.query.blur = {r: radius, s: standard}
|
268 | return this
|
269 | }
|
270 |
|
271 | |
272 |
|
273 |
|
274 |
|
275 | bright(brightness) {
|
276 | if (typeof brightness !== 'number') {
|
277 | throw new AliImgError('bright() brightness 应为数字类型')
|
278 | }
|
279 | let origin = this.query.bright || 0
|
280 |
|
281 | brightness += origin
|
282 | let lowerLimit = -100
|
283 | let upperLimit = 100
|
284 |
|
285 | brightness = Math.max(Math.min(brightness, upperLimit), lowerLimit)
|
286 | this.query.bright = brightness
|
287 | return this
|
288 | }
|
289 |
|
290 | |
291 |
|
292 |
|
293 |
|
294 | contrast (value) {
|
295 | if (typeof value !== 'number') {
|
296 | throw new AliImgError('contrast() value 应为数字类型')
|
297 | }
|
298 |
|
299 | let origin = this.query.contrast || 0
|
300 |
|
301 | value += origin
|
302 | let lowerLimit = -100
|
303 | let upperLimit = 100
|
304 |
|
305 | value = Math.max(Math.min(value, upperLimit), lowerLimit)
|
306 | this.query.contrast = value
|
307 | return this
|
308 | }
|
309 |
|
310 | |
311 |
|
312 |
|
313 |
|
314 | sharpen (value) {
|
315 | if (typeof value !== 'number') {
|
316 | throw new AliImgError('sharpen() value 应为数字类型')
|
317 | }
|
318 |
|
319 | let origin = this.query.sharpen || 0
|
320 |
|
321 | value += origin
|
322 |
|
323 |
|
324 | let lowerLimit = 50
|
325 | let upperLimit = 399
|
326 |
|
327 | value = Math.max(Math.min(value, upperLimit), lowerLimit)
|
328 | this.query.sharpen = value
|
329 | return this
|
330 | }
|
331 |
|
332 | |
333 |
|
334 |
|
335 |
|
336 | format (type) {
|
337 | if (!TYPE_LIST.includes(type)) {
|
338 | throw new AliImgError('format() 不支持该输出格式')
|
339 | }
|
340 | this.query.format = type
|
341 | return this
|
342 | }
|
343 |
|
344 | |
345 |
|
346 |
|
347 |
|
348 | interlace (type) {
|
349 | if (typeof type !== 'number') {
|
350 | throw new AliImgError('interlace() type 应为数字类型')
|
351 | }
|
352 |
|
353 | type = Number(Boolean(type))
|
354 | this.query.interlace = type
|
355 | return this
|
356 | }
|
357 |
|
358 | |
359 |
|
360 |
|
361 |
|
362 | quality (value) {
|
363 | if (typeof value !== 'number') {
|
364 | throw new AliImgError('quality() value 应为数字类型')
|
365 | }
|
366 |
|
367 | let origin = this.query.quality ? this.query.quality.q : 100
|
368 |
|
369 | value = parseInt(origin / 100 * value)
|
370 |
|
371 |
|
372 | let lowerLimit = 1
|
373 | let upperLimit = 100
|
374 |
|
375 | value = Math.max(Math.min(value, upperLimit), lowerLimit)
|
376 | this.query.quality = { q: value }
|
377 | return this
|
378 | }
|
379 |
|
380 | |
381 |
|
382 |
|
383 |
|
384 | absQual (value) {
|
385 | if (typeof value !== 'number') {
|
386 | throw new AliImgError('absQual() value 应为数字类型')
|
387 | }
|
388 |
|
389 |
|
390 | let lowerLimit = 1
|
391 | let upperLimit = 100
|
392 |
|
393 | value = Math.max(Math.min(value, upperLimit), lowerLimit)
|
394 | this.query.quality = { Q: value }
|
395 | return this
|
396 | }
|
397 |
|
398 | |
399 |
|
400 |
|
401 |
|
402 |
|
403 | watermark (content, opt) {
|
404 | let param = {}
|
405 | opt = opt || {}
|
406 |
|
407 | if (typeof content === 'string') {
|
408 | if (Buffer.byteLength(content) > MAX_BYTE_SIZE) {
|
409 | throw new AliImgError(`watermark() content 文本最多支持${MAX_BYTE_SIZE}字节`)
|
410 | }
|
411 | param.text = this.toBase64(content)
|
412 | } else if (content instanceof Img) {
|
413 | this.addChild(content.child)
|
414 | param.image = this.toBase64(content.toString())
|
415 | } else {
|
416 | throw new AliImgError('watermark() content 应为字符串或 Img 实例')
|
417 | }
|
418 | if (typeof opt !== 'object') {
|
419 | throw new AliImgError('watermark() opt 应为对象')
|
420 | }
|
421 |
|
422 |
|
423 | let lowerLimit = 0
|
424 | let upperLimit = 4096
|
425 |
|
426 |
|
427 | if (typeof opt.x === 'number') {
|
428 | param.x = Math.max(Math.min(opt.x, upperLimit), lowerLimit)
|
429 | } else if (opt.x) {
|
430 | throw new AliImgError('watermark() x 应为数字类型')
|
431 | }
|
432 |
|
433 |
|
434 | if (typeof opt.y === 'number') {
|
435 | param.y = Math.max(Math.min(opt.y, upperLimit), lowerLimit)
|
436 | } else if (opt.y) {
|
437 | throw new AliImgError('watermark() y 应为数字类型')
|
438 | }
|
439 |
|
440 |
|
441 | if (ORIGIN_LIST.includes(opt.position)) {
|
442 | param.g = opt.position
|
443 | } else if (opt.position) {
|
444 | throw new AliImgError('watermark() position 值不支持')
|
445 | }
|
446 |
|
447 |
|
448 | if (typeof opt.transparency === 'number') {
|
449 |
|
450 | let lowerLimit = 0
|
451 | let upperLimit = 100
|
452 |
|
453 | param.t = Math.max(Math.min(opt.transparency, upperLimit), lowerLimit)
|
454 | } else if (opt.transparency) {
|
455 | throw new AliImgError('watermark() transparency 应为数字类型')
|
456 | }
|
457 |
|
458 |
|
459 | if (typeof opt.voffset === 'number') {
|
460 |
|
461 | let lowerLimit = -1000
|
462 | let upperLimit = 1000
|
463 |
|
464 | param.voffset = Math.max(Math.min(opt.voffset, upperLimit), lowerLimit)
|
465 | } else if (opt.voffset) {
|
466 | throw new AliImgError('watermark() voffset 应为数字类型')
|
467 | }
|
468 |
|
469 |
|
470 | if (typeof opt.fill === 'number') {
|
471 |
|
472 | param.fill = Number(Boolean(opt.fill))
|
473 | } else if (opt.fill) {
|
474 | throw new AliImgError('watermark() fill 应为数字类型')
|
475 | }
|
476 |
|
477 |
|
478 | ['color', 'size', 'type', 'shadow', 'rotate'].forEach(key => {
|
479 | if (this.style[key]) {
|
480 | param[key] = this.style[key]
|
481 | }
|
482 | })
|
483 |
|
484 | this.query.watermark = this.query.watermark || []
|
485 | this.query.watermark.push(param)
|
486 | return this
|
487 | }
|
488 |
|
489 | |
490 |
|
491 |
|
492 |
|
493 | fill (color) {
|
494 | if (typeof color !== 'string') {
|
495 | throw new AliImgError('fill() color 应为字符串')
|
496 | }
|
497 |
|
498 | if (color[0] === '#') {
|
499 | color = color.slice(1)
|
500 | }
|
501 |
|
502 | if (color.length === 3) {
|
503 | color = color.split('').map(char => char + char).join('')
|
504 | }
|
505 | if (!/[0-9A-F]{6}/i.test(color)) {
|
506 | throw new AliImgError('fill() color 应为6位16进制数')
|
507 | }
|
508 | this.style.color = color
|
509 | return this
|
510 | }
|
511 |
|
512 | |
513 |
|
514 |
|
515 |
|
516 | font (name) {
|
517 | if (FONTS_LIST.includes(name)) {
|
518 |
|
519 | this.style.type = this.toBase64(name)
|
520 | } else if (FONTS_LIST_CN.includes(name)) {
|
521 |
|
522 | const index = FONTS_LIST_CN.indexOf(name)
|
523 | this.style.type = this.toBase64(FONTS_LIST[index])
|
524 | } else {
|
525 | throw new AliImgError(`font() 不支持该字体 ${name}`)
|
526 | }
|
527 | return this
|
528 | }
|
529 |
|
530 | |
531 |
|
532 |
|
533 |
|
534 | fontSize (size) {
|
535 | if (typeof size !== 'number') {
|
536 | throw new AliImgError('fontSize() size 应为数字类型')
|
537 | }
|
538 |
|
539 |
|
540 | let lowerLimit = 1
|
541 | let upperLimit = 1000
|
542 |
|
543 | size = Math.max(Math.min(size, upperLimit), lowerLimit)
|
544 |
|
545 | size = parseInt(size)
|
546 | this.style.size = size
|
547 | return this
|
548 | }
|
549 |
|
550 | |
551 |
|
552 |
|
553 |
|
554 | textShadow(transparency) {
|
555 | if (typeof transparency !== 'number') {
|
556 | throw new AliImgError('textShadow() transparency 应为数字类型')
|
557 | }
|
558 |
|
559 |
|
560 | let lowerLimit = 1
|
561 | let upperLimit = 100
|
562 |
|
563 | this.style.shadow = Math.max(Math.min(transparency, upperLimit), lowerLimit)
|
564 | return this
|
565 | }
|
566 |
|
567 | |
568 |
|
569 |
|
570 |
|
571 | textRotate(angle) {
|
572 | if (typeof angle !== 'number') {
|
573 | throw new AliImgError('textRotate() angle 应为数字类型')
|
574 | }
|
575 |
|
576 |
|
577 | this.style.rotate = (angle % 360 + 360) % 360
|
578 | return this
|
579 | }
|
580 |
|
581 |
|
582 | toBase64 (str) {
|
583 | return Buffer.from(str).toString('base64').replace(/[+/]/g, (match) => {
|
584 | return match == '+' ? '-' : '_'
|
585 | })
|
586 | }
|
587 |
|
588 |
|
589 | getObjectName () {
|
590 | return 'temp/' + Date.now() + parseInt(Math.random() * 8999 + 1000) + '.jpg'
|
591 | }
|
592 |
|
593 |
|
594 | addChild (arr) {
|
595 | this.child = this.child.concat(arr)
|
596 | }
|
597 |
|
598 |
|
599 | stringify () {
|
600 |
|
601 | let fmt = function(key, value) {
|
602 | let param = ''
|
603 | if (typeof value === 'object') {
|
604 | param = querystring.stringify(value, ',', '_', {
|
605 | encodeURIComponent: encodeURI
|
606 | })
|
607 | } else {
|
608 | param = value
|
609 | }
|
610 | return `image/${key},${param}`
|
611 | }
|
612 |
|
613 | return Object.keys(this.query).map((key) => {
|
614 | let value = this.query[key]
|
615 | if (Array.isArray(value)) {
|
616 | return value.map(fmt.bind(null, key)).join(',')
|
617 | } else {
|
618 | return fmt(key, value)
|
619 | }
|
620 | }).join(',')
|
621 | }
|
622 |
|
623 | toString () {
|
624 | return this.data.objectName + '?x-oss-process=' + this.stringify()
|
625 | }
|
626 | }
|
627 |
|
628 | Object.assign(Img, { TYPE_LIST, FONTS_LIST, FONTS_LIST_CN, ORIGIN_LIST, RESIZE_MODE })
|
629 |
|
630 | module.exports = Img |
\ | No newline at end of file |