mediac 1.7.5 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,16 +13,21 @@ import * as cliProgress from "cli-progress"
13
13
  import dayjs from "dayjs"
14
14
  import exif from 'exif-reader'
15
15
  import fs from 'fs-extra'
16
+ import imageSizeOfSync from 'image-size'
16
17
  import inquirer from "inquirer"
17
18
  import { cpus } from "os"
18
19
  import pMap from 'p-map'
19
20
  import path from "path"
20
21
  import sharp from "sharp"
22
+ import util from 'util'
21
23
  import * as core from '../lib/core.js'
22
24
  import * as log from '../lib/debug.js'
23
25
  import * as mf from '../lib/file.js'
24
26
  import * as helper from '../lib/helper.js'
25
- import { compressImage } from "./cmd_shared.js"
27
+ import tryfp from '../lib/tryfp.js'
28
+ import { calculateScale, compressImage } from "./cmd_shared.js"
29
+
30
+ //
26
31
  export { aliases, builder, command, describe, handler }
27
32
 
28
33
  const command = "compress <input> [output]"
@@ -34,18 +39,38 @@ const SIZE_DEFAULT = 2048 // in kbytes
34
39
  const WIDTH_DEFAULT = 6000
35
40
 
36
41
  const builder = function addOptions(ya, helpOrVersionSet) {
37
- return ya.option("purge", {
38
- alias: "p",
39
- type: "boolean",
40
- default: false,
41
- description: "Purge original image files",
42
- })
42
+ return ya
43
+ .option("purge", {
44
+ alias: "p",
45
+ type: "boolean",
46
+ default: false,
47
+ description: "Purge original image files",
48
+ })
49
+ // 输出目录,默认输出文件与原文件同目录
50
+ .option("output", {
51
+ alias: "o",
52
+ describe: "Folder store ouput files",
53
+ type: "string",
54
+ })
55
+ // 压缩后的文件后缀,默认为 _Z4K
56
+ .option("suffix", {
57
+ alias: "S",
58
+ describe: "filename suffix for compressed files",
59
+ type: "string",
60
+ default: "_Z4K",
61
+ })
43
62
  .option("purge-only", {
44
63
  type: "boolean",
45
64
  default: false,
46
65
  description: "Just delete original image files only",
47
66
  })
48
67
  // 是否覆盖已存在的压缩后文件
68
+ .option("force", {
69
+ type: "boolean",
70
+ default: false,
71
+ description: "Force compress all files",
72
+ })
73
+ // 是否覆盖已存在的压缩后文件
49
74
  .option("override", {
50
75
  type: "boolean",
51
76
  default: false,
@@ -72,6 +97,12 @@ const builder = function addOptions(ya, helpOrVersionSet) {
72
97
  default: WIDTH_DEFAULT,
73
98
  description: "Max width of long side of image thumb",
74
99
  })
100
+ // 并行操作限制,并发数,默认为 CPU 核心数
101
+ .option("jobs", {
102
+ alias: "j",
103
+ describe: "multi jobs running parallelly",
104
+ type: "number",
105
+ })
75
106
  // 确认执行所有系统操作,非测试模式,如删除和重命名和移动操作
76
107
  .option("doit", {
77
108
  alias: "d",
@@ -81,7 +112,10 @@ const builder = function addOptions(ya, helpOrVersionSet) {
81
112
  })
82
113
  }
83
114
 
84
- const handler = async function cmdCompress(argv) {
115
+
116
+ const handler = cmdCompress
117
+
118
+ async function cmdCompress(argv) {
85
119
  const testMode = !argv.doit
86
120
  const logTag = "cmdCompress"
87
121
  const root = path.resolve(argv.input)
@@ -102,15 +136,15 @@ const handler = async function cmdCompress(argv) {
102
136
  const purgeOnly = argv.purgeOnly || false
103
137
  const purgeSource = argv.purge || false
104
138
  log.show(`${logTag} input:`, root)
105
-
106
- const RE_THUMB = /Z4K|P4K|M4K|feature|web|thumb$/i
139
+ // 如果有force标志,就不过滤文件名
140
+ const RE_THUMB = argv.force ? /@_@/ : /Z4K|P4K|M4K|feature|web|thumb$/i
107
141
  const walkOpts = {
108
142
  needStats: true,
109
143
  entryFilter: (f) =>
110
144
  f.isFile
145
+ && helper.isImageFile(f.path)
111
146
  && !RE_THUMB.test(f.path)
112
147
  && f.size > minFileSize
113
- && helper.isImageFile(f.path)
114
148
  }
115
149
  log.showGreen(logTag, `Walking files ...`)
116
150
  let files = await mf.walk(root, walkOpts)
@@ -141,6 +175,9 @@ const handler = async function cmdCompress(argv) {
141
175
  const addArgsFunc = async (f, i) => {
142
176
  return {
143
177
  ...f,
178
+ force: argv.force || false,
179
+ suffix: argv.suffix,
180
+ output: argv.output,
144
181
  total: files.length,
145
182
  index: i,
146
183
  quality,
@@ -154,7 +191,7 @@ const handler = async function cmdCompress(argv) {
154
191
  t.needBar = needBar
155
192
  })
156
193
  needBar && bar1.start(files.length, 0)
157
- let tasks = await pMap(files, preCompress, { concurrency: cpus().length * 4 })
194
+ let tasks = await pMap(files, preCompress, { concurrency: argv.jobs || cpus().length * 4 })
158
195
  needBar && bar1.update(files.length)
159
196
  needBar && bar1.stop()
160
197
  log.info(logTag, "before filter: ", tasks.length)
@@ -233,13 +270,20 @@ let compressLastUpdatedAt = 0
233
270
  const bar1 = new cliProgress.SingleBar({ etaBuffer: 300 }, cliProgress.Presets.shades_classic)
234
271
  // 文心一言注释 20231206
235
272
  // 准备压缩图片的参数,并进行相应的处理
236
- async function preCompress(f, options = {}) {
273
+ async function preCompress(f) {
237
274
  const logTag = 'PreCompress'
238
275
  const maxWidth = f.maxWidth || 6000 // 获取最大宽度限制,默认为6000
239
276
  let fileSrc = path.resolve(f.path) // 解析源文件路径
240
277
  const [dir, base, ext] = helper.pathSplit(fileSrc) // 将路径分解为目录、基本名和扩展名
241
- const fileDstTmp = path.join(dir, `_TMP_${base}.jpg`)
242
- let fileDst = path.join(dir, `${base}_Z4K.jpg`) // 构建目标文件路径,添加压缩后的文件名后缀
278
+ const suffix = f.suffix || "_Z4K"
279
+ log.info(logTag, 'Processing ', fileSrc, suffix)
280
+
281
+ let fileDstDir = f.output ? helper.pathRewrite(f.root, dir, f.output, false) : dir
282
+ const tempSuffix = `_tmp@${helper.textHash(fileSrc)}@tmp_`
283
+ const fileDstTmp = path.join(fileDstDir, `${base}${suffix}${tempSuffix}.jpg`)
284
+ // 构建目标文件路径,添加压缩后的文件名后缀
285
+ let fileDst = path.join(fileDstDir, `${base}${suffix}.jpg`)
286
+
243
287
  fileSrc = path.resolve(fileSrc) // 解析源文件路径(再次确认)
244
288
  fileDst = path.resolve(fileDst) // 解析目标文件路径(再次确认)
245
289
 
@@ -264,17 +308,21 @@ async function preCompress(f, options = {}) {
264
308
  skipReason: 'DST EXISTS',
265
309
  }
266
310
  }
267
- try {
268
- const st = await fs.stat(fileSrc)
269
- const m = await sharp(fileSrc).metadata()
270
- try {
311
+ let [err, im] = await core.tryRunAsync(async () => {
312
+ return await sharp(fileSrc).metadata()
313
+ })
314
+
315
+ if (err) {
316
+ [err, im] = await tryfp.tryCatchAsync(util.promisify(imageSizeOfSync))(fileSrc)
317
+ } else {
318
+ if (im?.exif) {
319
+ log.info(logTag, "force:", fileDst)
320
+ const md = exif(im.exif)?.Image
271
321
  // 跳过以前由mediac压缩过的图片,避免重复压缩
272
- // 可能需要添加一个命令行参数控制
273
- if (m?.exif) {
274
- const md = exif(m.exif)?.Image
322
+ if (!f.force) {
275
323
  if (md.Copyright?.includes("mediac")
276
324
  || md.Software?.includes("mediac")
277
- || md.Artist?.includes("mediac")) {
325
+ || md.Artist?.includes("mediac") && !f.force) {
278
326
  log.info(logTag, "skip:", fileDst)
279
327
  return {
280
328
  ...f,
@@ -287,33 +335,34 @@ async function preCompress(f, options = {}) {
287
335
  }
288
336
  }
289
337
  }
290
- } catch (error) {
291
- log.warn(logTag, "exif", error.message, fileSrc)
292
- log.fileLog(`ExifErr: <${fileSrc}> ${error.message}`, logTag)
293
338
  }
339
+ }
294
340
 
295
- const { dstWidth, dstHeight } = calculateImageScale(m.width, m.height, maxWidth)
296
- if (f.total < 1000 || f.index > f.total - 1000) {
297
- log.show(logTag, `${f.index}/${f.total}`,
298
- helper.pathShort(fileSrc),
299
- `${m.width}x${m.height}=>${dstWidth}x${dstHeight} ${helper.humanSize(st.size)}`
300
- )
301
- }
302
- log.fileLog(`Pre: ${f.index}/${f.total} <${fileSrc}> ` +
303
- `${dstWidth}x${dstHeight}) ${m.format} ${helper.humanSize(st.size)}`, logTag)
304
- return {
305
- ...f,
306
- srcWidth: m.width,
307
- srcHeight: m.height,
308
- width: dstWidth,
309
- height: dstHeight,
310
- src: fileSrc,
311
- dst: fileDst,
312
- tmpDst: fileDstTmp,
313
- }
314
- } catch (error) {
341
+ if (err) {
315
342
  log.warn(logTag, "sharp", error.message, fileSrc)
316
343
  log.fileLog(`SharpErr: ${f.index} <${fileSrc}> sharp:${error.message}`, logTag)
344
+ return
345
+ }
346
+
347
+ const { dstWidth, dstHeight } = calculateImageScale(im.width, im.height, maxWidth)
348
+ if (f.total < 1000 || f.index > f.total - 1000) {
349
+ log.show(logTag, `${f.index}/${f.total}`,
350
+ helper.pathShort(fileSrc),
351
+ `${im.width}x${im.height}=>${dstWidth}x${dstHeight} ${im.format || im.type} ${helper.humanSize(f.size)}`
352
+ )
353
+ log.showGray(logTag, `${f.index}/${f.total} DST:`, fileDst)
354
+ }
355
+ log.fileLog(`Pre: ${f.index}/${f.total} <${fileSrc}> ` +
356
+ `${dstWidth}x${dstHeight}) ${helper.humanSize(f.size)}`, logTag)
357
+ return {
358
+ ...f,
359
+ srcWidth: im.width,
360
+ srcHeight: im.height,
361
+ width: dstWidth,
362
+ height: dstHeight,
363
+ src: fileSrc,
364
+ dst: fileDst,
365
+ tmpDst: fileDstTmp,
317
366
  }
318
367
  }
319
368
 
@@ -356,18 +405,4 @@ async function purgeSrcFiles(results) {
356
405
  const deleted = await pMap(toDelete, deletecFunc, { concurrency: cpus().length * 8 })
357
406
  log.showCyan(logTag, `${deleted.filter(Boolean).length} files are safely removed`)
358
407
 
359
- }
360
-
361
- // 给定图片长宽,给定长边数值,计算缩放后的长宽,只缩小不放大
362
- function calculateImageScale(imgWidth, imgHeight, maxSide) {
363
- // 不需要缩放的情况
364
- if (imgWidth <= maxSide && imgHeight <= maxSide) {
365
- return { dstWidth: imgWidth, dstHeight: imgHeight }
366
- }
367
- // 计算缩放比例
368
- let scaleFactor = maxSide / Math.max(imgWidth, imgHeight)
369
- // 计算新的长宽
370
- let dstWidth = Math.round(imgWidth * scaleFactor)
371
- let dstHeight = Math.round(imgHeight * scaleFactor)
372
- return { dstWidth, dstHeight }
373
408
  }