ag-psd 33.0.0 → 34.0.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.
Files changed (86) hide show
  1. package/.v8-cache/v24.7.0-x64-ef5a0af0/05135617 +0 -0
  2. package/.v8-cache/v24.7.0-x64-ef5a0af0/226e2801 +0 -0
  3. package/.v8-cache/v24.7.0-x64-ef5a0af0/4511bacf +0 -0
  4. package/.v8-cache/v24.7.0-x64-ef5a0af0/68416cb2 +0 -0
  5. package/.v8-cache/v24.7.0-x64-ef5a0af0/71df9cb3 +0 -0
  6. package/.v8-cache/v24.7.0-x64-ef5a0af0/7ea06cea +0 -0
  7. package/.v8-cache/v24.7.0-x64-ef5a0af0/87cf2209 +0 -0
  8. package/.v8-cache/v24.7.0-x64-ef5a0af0/932f0770 +0 -0
  9. package/.v8-cache/v24.7.0-x64-ef5a0af0/acc36e66 +0 -0
  10. package/.v8-cache/v24.7.0-x64-ef5a0af0/b3c2fab7 +0 -0
  11. package/.v8-cache/v24.7.0-x64-ef5a0af0/c0b789d2 +0 -0
  12. package/.v8-cache/v24.7.0-x64-ef5a0af0/c314aece +0 -0
  13. package/.v8-cache/v24.7.0-x64-ef5a0af0/cfc49f4f +0 -0
  14. package/.v8-cache/v24.7.0-x64-ef5a0af0/d13d7610 +0 -0
  15. package/.v8-cache/v24.7.0-x64-ef5a0af0/dc86f000 +0 -0
  16. package/.v8-cache/v24.7.0-x64-ef5a0af0/e03e2acd +0 -0
  17. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6b99945 +0 -0
  18. package/CHANGELOG.md +22 -0
  19. package/README.md +1 -1
  20. package/README_PSD.md +25 -9
  21. package/TODO.md +13 -84
  22. package/dist/abr.js +12 -7
  23. package/dist/abr.js.map +1 -1
  24. package/dist/additionalInfo.d.ts +2 -0
  25. package/dist/additionalInfo.js +361 -75
  26. package/dist/additionalInfo.js.map +1 -1
  27. package/dist/bundle.js +1524 -389
  28. package/dist/descriptor.d.ts +14 -5
  29. package/dist/descriptor.js +102 -64
  30. package/dist/descriptor.js.map +1 -1
  31. package/dist/helpers.d.ts +6 -0
  32. package/dist/helpers.js +242 -2
  33. package/dist/helpers.js.map +1 -1
  34. package/dist/imageResources.js +84 -3
  35. package/dist/imageResources.js.map +1 -1
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/psd.d.ts +144 -2
  40. package/dist/psd.js.map +1 -1
  41. package/dist/psdReader.js +206 -110
  42. package/dist/psdReader.js.map +1 -1
  43. package/dist/psdWriter.d.ts +1 -0
  44. package/dist/psdWriter.js +336 -91
  45. package/dist/psdWriter.js.map +1 -1
  46. package/dist/text.d.ts +32 -1
  47. package/dist/text.js +58 -0
  48. package/dist/text.js.map +1 -1
  49. package/dist-es/abr.js +12 -7
  50. package/dist-es/abr.js.map +1 -1
  51. package/dist-es/additionalInfo.d.ts +2 -0
  52. package/dist-es/additionalInfo.js +361 -75
  53. package/dist-es/additionalInfo.js.map +1 -1
  54. package/dist-es/descriptor.d.ts +14 -5
  55. package/dist-es/descriptor.js +102 -64
  56. package/dist-es/descriptor.js.map +1 -1
  57. package/dist-es/helpers.d.ts +6 -0
  58. package/dist-es/helpers.js +242 -2
  59. package/dist-es/helpers.js.map +1 -1
  60. package/dist-es/imageResources.js +84 -3
  61. package/dist-es/imageResources.js.map +1 -1
  62. package/dist-es/index.d.ts +1 -0
  63. package/dist-es/index.js +1 -0
  64. package/dist-es/index.js.map +1 -1
  65. package/dist-es/psd.d.ts +144 -2
  66. package/dist-es/psd.js.map +1 -1
  67. package/dist-es/psdReader.js +206 -110
  68. package/dist-es/psdReader.js.map +1 -1
  69. package/dist-es/psdWriter.d.ts +1 -0
  70. package/dist-es/psdWriter.js +336 -91
  71. package/dist-es/psdWriter.js.map +1 -1
  72. package/dist-es/text.d.ts +32 -1
  73. package/dist-es/text.js +58 -0
  74. package/dist-es/text.js.map +1 -1
  75. package/package.json +1 -1
  76. package/perf.js +312 -9
  77. package/src/abr.ts +14 -7
  78. package/src/additionalInfo.ts +433 -87
  79. package/src/descriptor.ts +103 -58
  80. package/src/helpers.ts +262 -2
  81. package/src/imageResources.ts +107 -4
  82. package/src/index.ts +1 -0
  83. package/src/psd.ts +115 -2
  84. package/src/psdReader.ts +226 -108
  85. package/src/psdWriter.ts +345 -90
  86. package/src/text.ts +76 -1
package/perf.js CHANGED
@@ -1,12 +1,315 @@
1
- require('ts-node').register({});
2
- const { readPsd } = require('./src/index.ts');
3
- require('./src/initializeCanvas.ts');
4
- const { readFileSync } = require('fs');
1
+ // Measures decoding speed of all PSD/PSB files in the repository and resources/psd-files
2
+ // and saves the summary to results/perf/perf-<version>[-<tag>].json
3
+ //
4
+ // usage: npx tsc -p tsconfig.json && node perf.js [options]
5
+ // --iterations <n> timed runs per file, after one warmup run (default 3)
6
+ // --filter <text> only measure files with path containing text
7
+ // --tag <name> appended to summary file name (perf-<version>-<name>.json)
8
+ // --compare <file> summary file (or version/tag, e.g. 33.0.0) to compare with,
9
+ // defaults to most recently modified other summary in results/perf
10
+ // --threshold <n> relative change reported as regression/improvement (default 0.1 = 10%)
5
11
 
6
- const buffer = readFileSync('test/read/ignore-stuck/src.psd');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const crypto = require('crypto');
15
+ const os = require('os');
16
+ const v8 = require('v8');
17
+ const vm = require('vm');
7
18
 
8
- const start = performance.now();
9
- const psd = readPsd(buffer);
10
- const end = performance.now();
19
+ const root = __dirname;
20
+ const perfDir = path.join(root, 'results', 'perf');
21
+ const excludedDirs = ['node_modules', 'dist', 'dist-es', 'results', 'resources', 'coverage', 'coverage-remapped', '.git'];
22
+ const readOptions = { useImageData: true, log: () => { } };
23
+ const minDiffMs = 2; // ignore smaller absolute changes as noise
11
24
 
12
- console.log('time', end - start, 'ms', psd.width, psd.height);
25
+ function parseArgs() {
26
+ const args = { iterations: 3, filter: '', tag: '', compare: '', threshold: 0.1 };
27
+ const argv = process.argv.slice(2);
28
+
29
+ for (let i = 0; i < argv.length; i++) {
30
+ const key = argv[i].replace(/^--/, '');
31
+ if (!(key in args)) throw new Error(`Unknown option: ${argv[i]}`);
32
+ const value = argv[++i];
33
+ if (value === undefined) throw new Error(`Missing value for: ${argv[i - 1]}`);
34
+ args[key] = typeof args[key] === 'number' ? +value : value;
35
+ }
36
+
37
+ return args;
38
+ }
39
+
40
+ function findFiles(dir, result = []) {
41
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
42
+ const fullPath = path.join(dir, entry.name);
43
+
44
+ if (entry.isDirectory()) {
45
+ if (!excludedDirs.includes(entry.name)) findFiles(fullPath, result);
46
+ } else if (/\.ps[db]$/i.test(entry.name)) {
47
+ result.push(fullPath);
48
+ }
49
+ }
50
+
51
+ return result;
52
+ }
53
+
54
+ function collectFiles(filter) {
55
+ const repoFiles = findFiles(root).sort();
56
+ const resourcesDir = path.join(root, 'resources', 'psd-files');
57
+ const resourceFiles = fs.existsSync(resourcesDir) ? findFiles(resourcesDir).sort() : [];
58
+ const hashes = new Set();
59
+ const files = [];
60
+
61
+ for (const file of [...repoFiles, ...resourceFiles]) {
62
+ const relative = path.relative(root, file).replace(/\\/g, '/');
63
+ if (filter && !relative.includes(filter)) continue;
64
+
65
+ const hash = crypto.createHash('sha1').update(fs.readFileSync(file)).digest('hex');
66
+ if (hashes.has(hash)) continue; // same file exists in both locations
67
+
68
+ hashes.add(hash);
69
+ files.push({ path: relative, fullPath: file, hash });
70
+ }
71
+
72
+ return files;
73
+ }
74
+
75
+ function checkBuild() {
76
+ const index = path.join(root, 'dist', 'index.js');
77
+
78
+ if (!fs.existsSync(index)) {
79
+ console.error('dist/index.js not found, build first: npx tsc -p tsconfig.json');
80
+ process.exit(1);
81
+ }
82
+
83
+ const buildTime = fs.statSync(index).mtimeMs;
84
+ const stale = fs.readdirSync(path.join(root, 'src'))
85
+ .filter(f => f.endsWith('.ts') && fs.statSync(path.join(root, 'src', f)).mtimeMs > buildTime);
86
+
87
+ if (stale.length) console.warn(`WARNING: dist/ is older than source files (${stale.join(', ')}), rebuild first: npx tsc -p tsconfig.json\n`);
88
+ }
89
+
90
+ function getGC() {
91
+ v8.setFlagsFromString('--expose-gc');
92
+ return vm.runInNewContext('gc');
93
+ }
94
+
95
+ function median(values) {
96
+ const sorted = [...values].sort((a, b) => a - b);
97
+ const mid = sorted.length >> 1;
98
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
99
+ }
100
+
101
+ const round = (value, digits = 2) => +value.toFixed(digits);
102
+
103
+ function formatSize(bytes) {
104
+ if (bytes < 1024) return `${bytes} B`;
105
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
106
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
107
+ }
108
+
109
+ function measure(readPsd, file, iterations, gc) {
110
+ const buffer = fs.readFileSync(file.fullPath);
111
+ const result = { path: file.path, hash: file.hash, size: buffer.byteLength, status: 'ok' };
112
+ const times = [];
113
+
114
+ for (let i = 0; i <= iterations; i++) {
115
+ gc();
116
+ const start = performance.now();
117
+ let psd;
118
+
119
+ try {
120
+ psd = readPsd(buffer, readOptions);
121
+ } catch (e) {
122
+ result.status = 'error';
123
+ result.error = `${e.message || e}`.split('\n')[0].slice(0, 200);
124
+ }
125
+
126
+ const time = performance.now() - start;
127
+ if (i > 0) times.push(time); // first run is warmup
128
+
129
+ if (psd && i === 0) {
130
+ result.width = psd.width;
131
+ result.height = psd.height;
132
+ result.layers = countLayers(psd);
133
+ }
134
+
135
+ psd = undefined;
136
+ }
137
+
138
+ result.min = round(Math.min(...times), 3);
139
+ result.median = round(median(times), 3);
140
+ result.times = times.map(t => round(t, 3));
141
+ return result;
142
+ }
143
+
144
+ function countLayers(parent) {
145
+ let count = 0;
146
+
147
+ for (const child of parent.children || []) {
148
+ count += 1 + countLayers(child);
149
+ }
150
+
151
+ return count;
152
+ }
153
+
154
+ function summarize(results, elapsed) {
155
+ const ok = results.filter(r => r.status === 'ok');
156
+ const totalBytes = ok.reduce((sum, r) => sum + r.size, 0);
157
+ const totalMin = ok.reduce((sum, r) => sum + r.min, 0);
158
+ const totalMedian = ok.reduce((sum, r) => sum + r.median, 0);
159
+
160
+ return {
161
+ files: results.length,
162
+ ok: ok.length,
163
+ failed: results.length - ok.length,
164
+ totalBytes,
165
+ totalMinMs: round(totalMin),
166
+ totalMedianMs: round(totalMedian),
167
+ throughputMBps: round(totalBytes / (1024 * 1024) / (totalMin / 1000)),
168
+ failedTotalMinMs: round(results.filter(r => r.status !== 'ok').reduce((sum, r) => sum + r.min, 0)),
169
+ elapsedMs: round(elapsed),
170
+ };
171
+ }
172
+
173
+ function findCompareFile(compare, outputFile) {
174
+ if (compare) {
175
+ const candidates = [compare, path.join(perfDir, compare), path.join(perfDir, `perf-${compare}.json`)];
176
+ const found = candidates.find(f => fs.existsSync(f) && fs.statSync(f).isFile());
177
+ if (!found) throw new Error(`Summary to compare not found: ${compare}`);
178
+ return found;
179
+ }
180
+
181
+ if (!fs.existsSync(perfDir)) return undefined;
182
+
183
+ return fs.readdirSync(perfDir)
184
+ .filter(f => /^perf-.*\.json$/.test(f))
185
+ .map(f => path.join(perfDir, f))
186
+ .filter(f => path.resolve(f) !== path.resolve(outputFile))
187
+ .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0];
188
+ }
189
+
190
+ function printTable(rows, columns) {
191
+ const widths = columns.map((c, i) => Math.max(c.length, ...rows.map(r => `${r[i]}`.length)));
192
+ const line = cells => cells.map((c, i) => i === 0 ? `${c}`.padEnd(widths[i]) : `${c}`.padStart(widths[i])).join(' ');
193
+ console.log(line(columns));
194
+ for (const row of rows) console.log(line(row));
195
+ }
196
+
197
+ function compareSummaries(current, previousFile, threshold) {
198
+ const previous = JSON.parse(fs.readFileSync(previousFile, 'utf8'));
199
+ const previousByHash = new Map(previous.results.map(r => [r.hash, r]));
200
+ const changes = [];
201
+ const statusChanges = [];
202
+ let currentTotal = 0, previousTotal = 0, logRatioSum = 0;
203
+
204
+ for (const r of current.results) {
205
+ const p = previousByHash.get(r.hash);
206
+ if (!p) continue;
207
+
208
+ if (p.status !== r.status) {
209
+ statusChanges.push(`${r.path}: ${p.status} -> ${r.status}${r.error ? ` (${r.error})` : ''}`);
210
+ continue;
211
+ }
212
+
213
+ if (r.status !== 'ok') continue;
214
+
215
+ currentTotal += r.min;
216
+ previousTotal += p.min;
217
+ logRatioSum += Math.log(Math.max(r.min, 0.001) / Math.max(p.min, 0.001));
218
+ changes.push({ path: r.path, before: p.min, after: r.min, ratio: r.min / Math.max(p.min, 0.001) });
219
+ }
220
+
221
+ const significant = c => Math.abs(c.after - c.before) >= minDiffMs && Math.abs(c.ratio - 1) >= threshold;
222
+ const regressions = changes.filter(c => c.ratio > 1 && significant(c)).sort((a, b) => (b.after - b.before) - (a.after - a.before));
223
+ const improvements = changes.filter(c => c.ratio < 1 && significant(c)).sort((a, b) => (a.after - a.before) - (b.after - b.before));
224
+ const percent = ratio => `${ratio >= 1 ? '+' : ''}${((ratio - 1) * 100).toFixed(1)}%`;
225
+ const toRows = list => list.slice(0, 20).map(c => [c.path, c.before.toFixed(2), c.after.toFixed(2), percent(c.ratio)]);
226
+
227
+ console.log(`\nComparison with ${path.relative(root, previousFile)} (version ${previous.version}${previous.tag ? `-${previous.tag}` : ''}, ${previous.date})`);
228
+ console.log(` common files: ${changes.length}`);
229
+ console.log(` total (min): ${previousTotal.toFixed(1)} ms -> ${currentTotal.toFixed(1)} ms (${percent(currentTotal / Math.max(previousTotal, 0.001))})`);
230
+ console.log(` geometric mean of per-file ratios: ${percent(Math.exp(logRatioSum / Math.max(changes.length, 1)))}`);
231
+
232
+ if (previous.node !== current.node || previous.cpu !== current.cpu) {
233
+ console.log(` NOTE: different environment (${previous.node}, ${previous.cpu})`);
234
+ }
235
+
236
+ if (statusChanges.length) {
237
+ console.log(`\nStatus changes (${statusChanges.length}):`);
238
+ for (const s of statusChanges) console.log(` ${s}`);
239
+ }
240
+
241
+ if (regressions.length) {
242
+ console.log(`\nRegressions (${regressions.length}, showing top 20 by ms):`);
243
+ printTable(toRows(regressions), ['file', 'before', 'after', 'change']);
244
+ }
245
+
246
+ if (improvements.length) {
247
+ console.log(`\nImprovements (${improvements.length}, showing top 20 by ms):`);
248
+ printTable(toRows(improvements), ['file', 'before', 'after', 'change']);
249
+ }
250
+
251
+ if (!regressions.length && !improvements.length && !statusChanges.length) {
252
+ console.log(`\nNo significant changes (threshold ${threshold * 100}%, ${minDiffMs} ms)`);
253
+ }
254
+ }
255
+
256
+ function main() {
257
+ const args = parseArgs();
258
+ checkBuild();
259
+
260
+ require('./dist/initializeCanvas');
261
+ const { readPsd } = require('./dist/index');
262
+ const { version } = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
263
+ const outputFile = path.join(perfDir, `perf-${version}${args.tag ? `-${args.tag}` : ''}${args.filter ? '-filtered' : ''}.json`);
264
+ const gc = getGC();
265
+ const files = collectFiles(args.filter);
266
+ const results = [];
267
+ const start = performance.now();
268
+
269
+ console.log(`ag-psd ${version}, ${files.length} files, ${args.iterations} iterations, node ${process.version}`);
270
+
271
+ for (let i = 0; i < files.length; i++) {
272
+ if (process.stdout.isTTY) {
273
+ process.stdout.write(`\r[${i + 1}/${files.length}] ${files[i].path}`.slice(0, process.stdout.columns - 1).padEnd(process.stdout.columns - 1));
274
+ }
275
+
276
+ results.push(measure(readPsd, files[i], args.iterations, gc));
277
+ }
278
+
279
+ if (process.stdout.isTTY) process.stdout.write('\r'.padEnd(process.stdout.columns - 1) + '\r');
280
+
281
+ const summary = {
282
+ version,
283
+ tag: args.tag || undefined,
284
+ filter: args.filter || undefined,
285
+ date: new Date().toISOString(),
286
+ node: process.version,
287
+ platform: `${os.platform()} ${os.release()} ${os.arch()}`,
288
+ cpu: os.cpus()[0]?.model.trim(),
289
+ iterations: args.iterations,
290
+ readOptions: { useImageData: true },
291
+ total: summarize(results, performance.now() - start),
292
+ results,
293
+ };
294
+
295
+ fs.mkdirSync(perfDir, { recursive: true });
296
+ fs.writeFileSync(outputFile, JSON.stringify(summary, null, 2), 'utf8');
297
+
298
+ const { total } = summary;
299
+ console.log(`\nfiles: ${total.files} (${total.ok} ok, ${total.failed} failed)`);
300
+ console.log(`total size: ${formatSize(total.totalBytes)}`);
301
+ console.log(`total time: ${total.totalMinMs.toFixed(1)} ms (min), ${total.totalMedianMs.toFixed(1)} ms (median)`);
302
+ console.log(`throughput: ${total.throughputMBps} MB/s`);
303
+ console.log(`elapsed: ${(total.elapsedMs / 1000).toFixed(1)} s`);
304
+
305
+ console.log('\nSlowest files:');
306
+ const slowest = results.filter(r => r.status === 'ok').sort((a, b) => b.min - a.min).slice(0, 10);
307
+ printTable(slowest.map(r => [r.path, formatSize(r.size), r.min.toFixed(2), r.median.toFixed(2)]), ['file', 'size', 'min ms', 'median ms']);
308
+
309
+ console.log(`\nSummary saved to ${path.relative(root, outputFile)}`);
310
+
311
+ const compareFile = findCompareFile(args.compare, outputFile);
312
+ if (compareFile) compareSummaries(summary, compareFile, args.threshold);
313
+ }
314
+
315
+ main();
package/src/abr.ts CHANGED
@@ -677,7 +677,7 @@ function readSampleBitmap(reader: PsdReader, brushEnd: number, id: string): Samp
677
677
  throw new Error('Brush sample data out of bounds');
678
678
  }
679
679
 
680
- consumeMemory(reader, w * h * (compression === 1 && bithDepth === 16 ? 3 : 1));
680
+ consumeMemory(reader, w * h * (bithDepth === 16 ? 3 : 1));
681
681
  const alpha = new Uint8Array(w * h);
682
682
 
683
683
  if (bithDepth === 8) {
@@ -689,19 +689,26 @@ function readSampleBitmap(reader: PsdReader, brushEnd: number, id: string): Samp
689
689
  throw new Error('Invalid compression');
690
690
  }
691
691
  } else if (bithDepth === 16) {
692
+ const data = new Uint16Array(w * h);
693
+
692
694
  if (compression === 0) {
693
- for (let i = 0; i < alpha.byteLength; i++) {
694
- alpha[i] = readUint16(reader) >> 8; // convert to 8bit values
695
+ for (let i = 0; i < data.length; i++) {
696
+ data[i] = readUint16(reader);
695
697
  }
696
698
  } else if (compression === 1) {
697
- const data = new Uint16Array(w * h);
698
699
  readDataRLE(reader, { width: w, height: h, data }, w, h, bithDepth, 1, [0], false);
699
- for (let i = 0; i < alpha.byteLength; i++) {
700
- alpha[i] = data[i] >> 8; // convert to 8bit values
701
- }
702
700
  } else {
703
701
  throw new Error('Invalid compression');
704
702
  }
703
+
704
+ // Photoshop uses 0-32768 range for 16bit values internally, use full 0-65535 range only if there are values outside of it
705
+ let max = 0;
706
+ for (let i = 0; i < data.length; i++) max = Math.max(max, data[i]);
707
+ const scale = max > 32768 ? 255 / 65535 : 255 / 32768;
708
+
709
+ for (let i = 0; i < alpha.byteLength; i++) {
710
+ alpha[i] = Math.round(data[i] * scale); // convert to 8bit values
711
+ }
705
712
  } else {
706
713
  throw new Error('Invalid depth');
707
714
  }