svg-toolbox 1.1.11 → 1.2.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.
- package/README.md +156 -118
- package/es/analyze/__tests__/colors.test.d.ts +1 -0
- package/es/analyze/__tests__/colors.test.js +44 -0
- package/es/analyze/__tests__/colors.test.js.map +1 -0
- package/es/analyze/__tests__/paths.test.d.ts +1 -0
- package/es/analyze/__tests__/paths.test.js +71 -0
- package/es/analyze/__tests__/paths.test.js.map +1 -0
- package/es/analyze/colors.d.ts +8 -0
- package/es/analyze/colors.js +55 -0
- package/es/analyze/colors.js.map +1 -0
- package/es/analyze/paths.d.ts +20 -0
- package/es/analyze/paths.js +92 -0
- package/es/analyze/paths.js.map +1 -0
- package/es/compare/__tests__/diff.test.d.ts +1 -0
- package/es/compare/__tests__/diff.test.js +64 -0
- package/es/compare/__tests__/diff.test.js.map +1 -0
- package/es/compare/diff.d.ts +18 -0
- package/es/compare/diff.js +70 -0
- package/es/compare/diff.js.map +1 -0
- package/es/convert/__tests__/base64.test.d.ts +1 -0
- package/es/convert/__tests__/base64.test.js +49 -0
- package/es/convert/__tests__/base64.test.js.map +1 -0
- package/es/convert/base64.d.ts +11 -0
- package/es/convert/base64.js +38 -0
- package/es/convert/base64.js.map +1 -0
- package/es/convert/image.d.ts +18 -0
- package/es/convert/image.js +63 -0
- package/es/convert/image.js.map +1 -0
- package/es/core/__tests__/dimensions.test.d.ts +1 -0
- package/es/core/__tests__/dimensions.test.js +40 -0
- package/es/core/__tests__/dimensions.test.js.map +1 -0
- package/es/core/__tests__/element.test.d.ts +1 -0
- package/es/core/__tests__/element.test.js +79 -0
- package/es/core/__tests__/element.test.js.map +1 -0
- package/es/core/dimensions.d.ts +8 -0
- package/es/core/dimensions.js +34 -0
- package/es/core/dimensions.js.map +1 -0
- package/es/core/element.d.ts +24 -0
- package/es/core/element.js +52 -0
- package/es/core/element.js.map +1 -0
- package/es/index.d.ts +15 -10
- package/es/index.js +21 -10
- package/es/index.js.map +1 -1
- package/es/optimize/__tests__/cleanup.test.d.ts +1 -0
- package/es/optimize/__tests__/cleanup.test.js +95 -0
- package/es/optimize/__tests__/cleanup.test.js.map +1 -0
- package/es/optimize/__tests__/path.test.d.ts +1 -0
- package/es/optimize/__tests__/path.test.js +41 -0
- package/es/optimize/__tests__/path.test.js.map +1 -0
- package/es/optimize/cleanup.d.ts +27 -0
- package/es/optimize/cleanup.js +100 -0
- package/es/optimize/cleanup.js.map +1 -0
- package/es/optimize/path.d.ts +7 -0
- package/es/optimize/path.js +58 -0
- package/es/optimize/path.js.map +1 -0
- package/es/types/index.d.ts +31 -0
- package/es/types/index.js +5 -0
- package/es/types/index.js.map +1 -0
- package/es/utils/__tests__/path-validation.test.d.ts +1 -0
- package/es/utils/__tests__/path-validation.test.js +64 -0
- package/es/utils/__tests__/path-validation.test.js.map +1 -0
- package/es/utils/__tests__/validation.test.d.ts +1 -0
- package/es/utils/__tests__/validation.test.js +42 -0
- package/es/utils/__tests__/validation.test.js.map +1 -0
- package/es/utils/dom.d.ts +23 -0
- package/es/utils/dom.js +25 -0
- package/es/utils/dom.js.map +1 -0
- package/es/utils/path-validation.d.ts +29 -0
- package/es/utils/path-validation.js +101 -0
- package/es/utils/path-validation.js.map +1 -0
- package/es/utils/validation.d.ts +11 -0
- package/es/utils/validation.js +30 -0
- package/es/utils/validation.js.map +1 -0
- package/lib/analyze/__tests__/colors.test.d.ts +1 -0
- package/lib/analyze/__tests__/colors.test.js +46 -0
- package/lib/analyze/__tests__/colors.test.js.map +1 -0
- package/lib/analyze/__tests__/paths.test.d.ts +1 -0
- package/lib/analyze/__tests__/paths.test.js +73 -0
- package/lib/analyze/__tests__/paths.test.js.map +1 -0
- package/lib/analyze/colors.d.ts +8 -0
- package/lib/analyze/colors.js +58 -0
- package/lib/analyze/colors.js.map +1 -0
- package/lib/analyze/paths.d.ts +20 -0
- package/lib/analyze/paths.js +97 -0
- package/lib/analyze/paths.js.map +1 -0
- package/lib/compare/__tests__/diff.test.d.ts +1 -0
- package/lib/compare/__tests__/diff.test.js +128 -0
- package/lib/compare/__tests__/diff.test.js.map +1 -0
- package/lib/compare/diff.d.ts +18 -0
- package/lib/compare/diff.js +134 -0
- package/lib/compare/diff.js.map +1 -0
- package/lib/convert/__tests__/base64.test.d.ts +1 -0
- package/lib/convert/__tests__/base64.test.js +51 -0
- package/lib/convert/__tests__/base64.test.js.map +1 -0
- package/lib/convert/base64.d.ts +11 -0
- package/lib/convert/base64.js +42 -0
- package/lib/convert/base64.js.map +1 -0
- package/lib/convert/image.d.ts +18 -0
- package/lib/{extra-apply/applySvg2Png.js → convert/image.js} +62 -53
- package/lib/convert/image.js.map +1 -0
- package/lib/core/__tests__/dimensions.test.d.ts +1 -0
- package/lib/core/__tests__/dimensions.test.js +42 -0
- package/lib/core/__tests__/dimensions.test.js.map +1 -0
- package/lib/core/__tests__/element.test.d.ts +1 -0
- package/lib/core/__tests__/element.test.js +81 -0
- package/lib/core/__tests__/element.test.js.map +1 -0
- package/lib/core/dimensions.d.ts +8 -0
- package/lib/core/dimensions.js +37 -0
- package/lib/core/dimensions.js.map +1 -0
- package/lib/core/element.d.ts +24 -0
- package/lib/core/element.js +58 -0
- package/lib/core/element.js.map +1 -0
- package/lib/index.d.ts +15 -10
- package/lib/index.js +48 -20
- package/lib/index.js.map +1 -1
- package/lib/optimize/__tests__/cleanup.test.d.ts +1 -0
- package/lib/optimize/__tests__/cleanup.test.js +97 -0
- package/lib/optimize/__tests__/cleanup.test.js.map +1 -0
- package/lib/optimize/__tests__/path.test.d.ts +1 -0
- package/lib/optimize/__tests__/path.test.js +43 -0
- package/lib/optimize/__tests__/path.test.js.map +1 -0
- package/lib/optimize/cleanup.d.ts +27 -0
- package/lib/optimize/cleanup.js +106 -0
- package/lib/optimize/cleanup.js.map +1 -0
- package/lib/optimize/path.d.ts +7 -0
- package/lib/optimize/path.js +63 -0
- package/lib/optimize/path.js.map +1 -0
- package/lib/types/index.d.ts +31 -0
- package/lib/types/index.js +6 -0
- package/lib/types/index.js.map +1 -0
- package/lib/utils/__tests__/path-validation.test.d.ts +1 -0
- package/lib/utils/__tests__/path-validation.test.js +69 -0
- package/lib/utils/__tests__/path-validation.test.js.map +1 -0
- package/lib/utils/__tests__/validation.test.d.ts +1 -0
- package/lib/utils/__tests__/validation.test.js +44 -0
- package/lib/utils/__tests__/validation.test.js.map +1 -0
- package/lib/utils/dom.d.ts +23 -0
- package/lib/utils/dom.js +31 -0
- package/lib/utils/dom.js.map +1 -0
- package/lib/utils/path-validation.d.ts +29 -0
- package/lib/utils/path-validation.js +109 -0
- package/lib/utils/path-validation.js.map +1 -0
- package/lib/utils/validation.d.ts +11 -0
- package/lib/utils/validation.js +35 -0
- package/lib/utils/validation.js.map +1 -0
- package/package.json +17 -4
- package/es/common.d.ts +0 -68
- package/es/common.js +0 -110
- package/es/common.js.map +0 -1
- package/es/extra-apply/applyDiffSvg.d.ts +0 -11
- package/es/extra-apply/applyDiffSvg.js +0 -63
- package/es/extra-apply/applyDiffSvg.js.map +0 -1
- package/es/extra-apply/applyRemoveNanCoordinates.d.ts +0 -6
- package/es/extra-apply/applyRemoveNanCoordinates.js +0 -75
- package/es/extra-apply/applyRemoveNanCoordinates.js.map +0 -1
- package/es/extra-apply/applySvg2Png.d.ts +0 -16
- package/es/extra-apply/applySvg2Png.js +0 -64
- package/es/extra-apply/applySvg2Png.js.map +0 -1
- package/es/utils/pixelLevelDiffPng.d.ts +0 -4
- package/es/utils/pixelLevelDiffPng.js +0 -29
- package/es/utils/pixelLevelDiffPng.js.map +0 -1
- package/es/validate.d.ts +0 -14
- package/es/validate.js +0 -44
- package/es/validate.js.map +0 -1
- package/lib/common.d.ts +0 -68
- package/lib/common.js +0 -117
- package/lib/common.js.map +0 -1
- package/lib/extra-apply/applyDiffSvg.d.ts +0 -11
- package/lib/extra-apply/applyDiffSvg.js +0 -116
- package/lib/extra-apply/applyDiffSvg.js.map +0 -1
- package/lib/extra-apply/applyRemoveNanCoordinates.d.ts +0 -6
- package/lib/extra-apply/applyRemoveNanCoordinates.js +0 -78
- package/lib/extra-apply/applyRemoveNanCoordinates.js.map +0 -1
- package/lib/extra-apply/applySvg2Png.d.ts +0 -16
- package/lib/extra-apply/applySvg2Png.js.map +0 -1
- package/lib/utils/pixelLevelDiffPng.d.ts +0 -4
- package/lib/utils/pixelLevelDiffPng.js +0 -36
- package/lib/utils/pixelLevelDiffPng.js.map +0 -1
- package/lib/validate.d.ts +0 -14
- package/lib/validate.js +0 -48
- package/lib/validate.js.map +0 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG path analysis utilities
|
|
3
|
+
*/
|
|
4
|
+
import { JSDOM } from 'jsdom';
|
|
5
|
+
import { serializeSVG } from '../core/element';
|
|
6
|
+
import { isValidSvgString } from '../utils/validation';
|
|
7
|
+
/**
|
|
8
|
+
* Maximum allowed path data length to prevent ReDoS attacks
|
|
9
|
+
*/
|
|
10
|
+
const MAX_PATH_DATA_LENGTH = 100000; // 100KB should be sufficient for most SVG paths
|
|
11
|
+
/**
|
|
12
|
+
* Parses the 'd' attribute of a path element into command objects
|
|
13
|
+
*/
|
|
14
|
+
export function parsePathData(pathData) {
|
|
15
|
+
// Validate input length to prevent ReDoS attacks
|
|
16
|
+
if (pathData.length > MAX_PATH_DATA_LENGTH) {
|
|
17
|
+
throw new Error(`Path data length exceeds maximum allowed length of ${MAX_PATH_DATA_LENGTH} characters`);
|
|
18
|
+
}
|
|
19
|
+
const commands = [];
|
|
20
|
+
// Split by path commands (M, L, H, V, C, S, Q, T, A, Z)
|
|
21
|
+
const commandRegex = /([MmLlHhVvCcSsQqTtAaZz])((?:\s*-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\s*,?\s*)*)/g;
|
|
22
|
+
let match;
|
|
23
|
+
let iterations = 0;
|
|
24
|
+
const MAX_ITERATIONS = 10000; // Prevent infinite loops
|
|
25
|
+
while ((match = commandRegex.exec(pathData)) !== null) {
|
|
26
|
+
iterations++;
|
|
27
|
+
if (iterations > MAX_ITERATIONS) {
|
|
28
|
+
throw new Error('Path parsing exceeded maximum iterations. Path data may be malformed.');
|
|
29
|
+
}
|
|
30
|
+
const type = match[1];
|
|
31
|
+
const paramsStr = match[2].trim();
|
|
32
|
+
if (type.toLowerCase() === 'z') {
|
|
33
|
+
commands.push({ type, params: [] });
|
|
34
|
+
}
|
|
35
|
+
else if (paramsStr) {
|
|
36
|
+
const params = paramsStr
|
|
37
|
+
.split(/[\s,]+/)
|
|
38
|
+
.filter(p => p.trim() !== '')
|
|
39
|
+
.map(Number)
|
|
40
|
+
.filter(n => !isNaN(n));
|
|
41
|
+
commands.push({ type, params });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return commands;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Analyzes all paths in an SVG and returns their command structures
|
|
48
|
+
*/
|
|
49
|
+
export function analyzePaths(svgContent) {
|
|
50
|
+
const svgString = isValidSvgString(svgContent)
|
|
51
|
+
? svgContent
|
|
52
|
+
: serializeSVG(svgContent);
|
|
53
|
+
const dom = new JSDOM(svgString, {
|
|
54
|
+
contentType: 'image/svg+xml'
|
|
55
|
+
});
|
|
56
|
+
const document = dom.window.document;
|
|
57
|
+
const svgElement = document.querySelector('svg');
|
|
58
|
+
if (!svgElement) {
|
|
59
|
+
throw new Error('No SVG element found in the provided content.');
|
|
60
|
+
}
|
|
61
|
+
const paths = svgElement.querySelectorAll('path');
|
|
62
|
+
const pathAnalysis = new Map();
|
|
63
|
+
Array.from(paths).forEach((path, index) => {
|
|
64
|
+
const d = path.getAttribute('d');
|
|
65
|
+
if (d) {
|
|
66
|
+
const id = path.getAttribute('id') || `path-${index}`;
|
|
67
|
+
pathAnalysis.set(id, parsePathData(d));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return pathAnalysis;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Gets statistics about paths in an SVG
|
|
74
|
+
*/
|
|
75
|
+
export function getPathStatistics(svgContent) {
|
|
76
|
+
const pathAnalysis = analyzePaths(svgContent);
|
|
77
|
+
let totalCommands = 0;
|
|
78
|
+
const commandTypes = {};
|
|
79
|
+
pathAnalysis.forEach(commands => {
|
|
80
|
+
totalCommands += commands.length;
|
|
81
|
+
commands.forEach(cmd => {
|
|
82
|
+
const type = cmd.type.toUpperCase();
|
|
83
|
+
commandTypes[type] = (commandTypes[type] || 0) + 1;
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
totalPaths: pathAnalysis.size,
|
|
88
|
+
totalCommands,
|
|
89
|
+
commandTypes
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/analyze/paths.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { diffImages } from '../diff';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
describe('Image Diff', () => {
|
|
5
|
+
const fixturesDir = path.join(__dirname, '../../../test-fixtures');
|
|
6
|
+
const test1Path = path.join(fixturesDir, 'test1.svg');
|
|
7
|
+
const test2Path = path.join(fixturesDir, 'test2.svg');
|
|
8
|
+
const outputDir = path.join(fixturesDir, 'diff-output');
|
|
9
|
+
const diffImagePath = path.join(outputDir, 'diff-test1-test2.png');
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
// Ensure output directory exists
|
|
12
|
+
if (!fs.existsSync(outputDir)) {
|
|
13
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
afterAll(() => {
|
|
17
|
+
// Clean up: optionally remove the diff image after tests
|
|
18
|
+
// Uncomment the following line if you want to clean up after tests
|
|
19
|
+
// if (fs.existsSync(diffImagePath)) {
|
|
20
|
+
// fs.unlinkSync(diffImagePath);
|
|
21
|
+
// }
|
|
22
|
+
});
|
|
23
|
+
it('should generate diff image between two similar but different SVGs', async () => {
|
|
24
|
+
// Verify test fixtures exist
|
|
25
|
+
expect(fs.existsSync(test1Path)).toBe(true);
|
|
26
|
+
expect(fs.existsSync(test2Path)).toBe(true);
|
|
27
|
+
// Generate diff image
|
|
28
|
+
const result = await diffImages(test1Path, test2Path, diffImagePath, 0.1);
|
|
29
|
+
// Verify diff image was created
|
|
30
|
+
expect(fs.existsSync(diffImagePath)).toBe(true);
|
|
31
|
+
// Verify result contains expected properties
|
|
32
|
+
expect(result).toHaveProperty('diffPngBuffer');
|
|
33
|
+
expect(result).toHaveProperty('numDiffPixels');
|
|
34
|
+
expect(result.diffPngBuffer).toBeInstanceOf(Buffer);
|
|
35
|
+
expect(typeof result.numDiffPixels).toBe('number');
|
|
36
|
+
// Since the SVGs are different, there should be some differences
|
|
37
|
+
expect(result.numDiffPixels).toBeGreaterThan(0);
|
|
38
|
+
// Verify the saved file matches the buffer
|
|
39
|
+
const savedFile = fs.readFileSync(diffImagePath);
|
|
40
|
+
expect(savedFile.equals(result.diffPngBuffer)).toBe(true);
|
|
41
|
+
// Verify file size is reasonable (not empty)
|
|
42
|
+
expect(savedFile.length).toBeGreaterThan(0);
|
|
43
|
+
console.log(`Diff image saved to: ${diffImagePath}`);
|
|
44
|
+
console.log(`Number of different pixels: ${result.numDiffPixels}`);
|
|
45
|
+
});
|
|
46
|
+
it('should handle identical SVGs (no differences)', async () => {
|
|
47
|
+
const identicalDiffPath = path.join(outputDir, 'diff-identical.png');
|
|
48
|
+
// Compare test1 with itself
|
|
49
|
+
const result = await diffImages(test1Path, test1Path, identicalDiffPath, 0.1);
|
|
50
|
+
// Should have no differences
|
|
51
|
+
expect(result.numDiffPixels).toBe(0);
|
|
52
|
+
expect(fs.existsSync(identicalDiffPath)).toBe(true);
|
|
53
|
+
console.log(`Identical comparison diff saved to: ${identicalDiffPath}`);
|
|
54
|
+
console.log(`Number of different pixels: ${result.numDiffPixels}`);
|
|
55
|
+
});
|
|
56
|
+
it('should generate diff without saving to file', async () => {
|
|
57
|
+
const result = await diffImages(test1Path, test2Path, undefined, 0.1);
|
|
58
|
+
// Should still return result even without file path
|
|
59
|
+
expect(result).toHaveProperty('diffPngBuffer');
|
|
60
|
+
expect(result).toHaveProperty('numDiffPixels');
|
|
61
|
+
expect(result.numDiffPixels).toBeGreaterThan(0);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
//# sourceMappingURL=diff.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.test.js","sourceRoot":"","sources":["../../../src/compare/__tests__/diff.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image comparison and diff utilities
|
|
3
|
+
*/
|
|
4
|
+
import { DiffResult } from '../types';
|
|
5
|
+
/**
|
|
6
|
+
* Compares two PNG images at the pixel level and generates a diff image
|
|
7
|
+
*/
|
|
8
|
+
export declare function pixelLevelDiff(pngA: Buffer, pngB: Buffer, threshold?: number): DiffResult;
|
|
9
|
+
/**
|
|
10
|
+
* Compares two image files (SVG or PNG) and generates a diff image
|
|
11
|
+
*/
|
|
12
|
+
export declare function diffImages(pathA: string, pathB: string, diffFilePath?: string, threshold?: number): Promise<DiffResult>;
|
|
13
|
+
/**
|
|
14
|
+
* Legacy function name for diffImages
|
|
15
|
+
*
|
|
16
|
+
* @deprecated Use diffImages instead
|
|
17
|
+
*/
|
|
18
|
+
export declare function diffSvg(pathA: string, pathB: string, diffFilePath: string): Promise<DiffResult | void>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image comparison and diff utilities
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import sharp from 'sharp';
|
|
7
|
+
import { PNG } from 'pngjs';
|
|
8
|
+
import pixelmatch from 'pixelmatch';
|
|
9
|
+
import { validateReadPath, validateWritePath } from '../utils/path-validation';
|
|
10
|
+
/**
|
|
11
|
+
* Compares two PNG images at the pixel level and generates a diff image
|
|
12
|
+
*/
|
|
13
|
+
export function pixelLevelDiff(pngA, pngB, threshold = 0.1) {
|
|
14
|
+
const img1 = PNG.sync.read(pngA);
|
|
15
|
+
const img2 = PNG.sync.read(pngB);
|
|
16
|
+
const { width, height } = img1;
|
|
17
|
+
if (img2.width !== width || img2.height !== height) {
|
|
18
|
+
throw new Error('Images must have the same dimensions');
|
|
19
|
+
}
|
|
20
|
+
const diff = new PNG({ width, height });
|
|
21
|
+
const numDiffPixels = pixelmatch(img1.data, img2.data, diff.data, width, height, { threshold });
|
|
22
|
+
const diffPngBuffer = PNG.sync.write(diff);
|
|
23
|
+
return {
|
|
24
|
+
diffPngBuffer,
|
|
25
|
+
numDiffPixels
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Compares two image files (SVG or PNG) and generates a diff image
|
|
30
|
+
*/
|
|
31
|
+
export async function diffImages(pathA, pathB, diffFilePath, threshold = 0.1) {
|
|
32
|
+
// Validate and normalize file paths to prevent path traversal
|
|
33
|
+
const validatedPathA = validateReadPath(pathA, ['.svg', '.png', '.jpg', '.jpeg', '.webp']);
|
|
34
|
+
const validatedPathB = validateReadPath(pathB, ['.svg', '.png', '.jpg', '.jpeg', '.webp']);
|
|
35
|
+
if (!fs.existsSync(validatedPathA)) {
|
|
36
|
+
throw new Error(`File not found: ${validatedPathA}`);
|
|
37
|
+
}
|
|
38
|
+
if (!fs.existsSync(validatedPathB)) {
|
|
39
|
+
throw new Error(`File not found: ${validatedPathB}`);
|
|
40
|
+
}
|
|
41
|
+
// Convert both images to PNG buffers
|
|
42
|
+
const pngA = await sharp(validatedPathA).png().toBuffer();
|
|
43
|
+
const pngB = await sharp(validatedPathB).png().toBuffer();
|
|
44
|
+
const result = pixelLevelDiff(pngA, pngB, threshold);
|
|
45
|
+
if (diffFilePath) {
|
|
46
|
+
const ext = path.extname(diffFilePath);
|
|
47
|
+
if (!ext) {
|
|
48
|
+
throw new Error('Diff file path must have a file extension');
|
|
49
|
+
}
|
|
50
|
+
// Validate write path to prevent path traversal
|
|
51
|
+
const validatedWritePath = validateWritePath(diffFilePath, ['.png']);
|
|
52
|
+
fs.writeFileSync(validatedWritePath, result.diffPngBuffer);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Legacy function name for diffImages
|
|
58
|
+
*
|
|
59
|
+
* @deprecated Use diffImages instead
|
|
60
|
+
*/
|
|
61
|
+
export async function diffSvg(pathA, pathB, diffFilePath) {
|
|
62
|
+
try {
|
|
63
|
+
return await diffImages(pathA, pathB, diffFilePath);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error(`Error comparing images: ${error instanceof Error ? error.message : String(error)}`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=diff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/compare/diff.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE/E;;GAEG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAY,EACZ,IAAY,EACZ,YAAoB,GAAG;IAEvB,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAE/B,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IACxC,MAAM,aAAa,GAAG,UAAU,CAC9B,IAAI,CAAC,IAAI,EACT,IAAI,CAAC,IAAI,EACT,IAAI,CAAC,IAAI,EACT,KAAK,EACL,MAAM,EACN,EAAE,SAAS,EAAE,CACd,CAAC;IAEF,MAAM,aAAa,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAE3C,OAAO;QACL,aAAa;QACb,aAAa;KACd,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,KAAa,EACb,KAAa,EACb,YAAqB,EACrB,YAAoB,GAAG;IAEvB,8DAA8D;IAC9D,MAAM,cAAc,GAAG,gBAAgB,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3F,MAAM,cAAc,GAAG,gBAAgB,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IAE3F,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,mBAAmB,cAAc,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,mBAAmB,cAAc,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,qCAAqC;IACrC,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,cAAc,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;IAC1D,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,cAAc,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;IAE1D,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;IAErD,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;QACvC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;QAC/D,CAAC;QACD,gDAAgD;QAChD,MAAM,kBAAkB,GAAG,iBAAiB,CAAC,YAAY,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;QACrE,EAAE,CAAC,aAAa,CAAC,kBAAkB,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC;IAC7D,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,KAAa,EACb,KAAa,EACb,YAAoB;IAEpB,IAAI,CAAC;QACH,OAAO,MAAM,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC;IACtD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,2BAA2B,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACnG,OAAO;IACT,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { convertSVGToBase64, convertBase64ToSVG } from '../base64';
|
|
2
|
+
import { createSVGElement } from '../../core/element';
|
|
3
|
+
describe('Base64 Conversion', () => {
|
|
4
|
+
const testSvg = '<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" /></svg>';
|
|
5
|
+
describe('convertSVGToBase64', () => {
|
|
6
|
+
it('should convert SVG string to base64 data URI', () => {
|
|
7
|
+
const base64 = convertSVGToBase64(testSvg);
|
|
8
|
+
expect(base64).toMatch(/^data:image\/svg\+xml;base64,/);
|
|
9
|
+
expect(base64.length).toBeGreaterThan(50);
|
|
10
|
+
});
|
|
11
|
+
it('should convert SVG element to base64 data URI', () => {
|
|
12
|
+
const element = createSVGElement(testSvg);
|
|
13
|
+
const base64 = convertSVGToBase64(element);
|
|
14
|
+
expect(base64).toMatch(/^data:image\/svg\+xml;base64,/);
|
|
15
|
+
});
|
|
16
|
+
it('should produce consistent results for same input', () => {
|
|
17
|
+
const base64_1 = convertSVGToBase64(testSvg);
|
|
18
|
+
const base64_2 = convertSVGToBase64(testSvg);
|
|
19
|
+
expect(base64_1).toBe(base64_2);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe('convertBase64ToSVG', () => {
|
|
23
|
+
it('should convert base64 data URI back to SVG string', () => {
|
|
24
|
+
const base64 = convertSVGToBase64(testSvg);
|
|
25
|
+
const svgString = convertBase64ToSVG(base64);
|
|
26
|
+
expect(svgString).toContain('<svg');
|
|
27
|
+
expect(svgString).toContain('circle');
|
|
28
|
+
});
|
|
29
|
+
it('should handle base64 string without data URI prefix', () => {
|
|
30
|
+
const base64 = convertSVGToBase64(testSvg);
|
|
31
|
+
const base64Only = base64.split(',')[1];
|
|
32
|
+
const svgString = convertBase64ToSVG(base64Only);
|
|
33
|
+
expect(svgString).toContain('<svg');
|
|
34
|
+
});
|
|
35
|
+
it('should throw error for invalid base64 string', () => {
|
|
36
|
+
expect(() => convertBase64ToSVG('invalid-base64!!!')).toThrow('Invalid Base64 string');
|
|
37
|
+
});
|
|
38
|
+
it('should round-trip correctly', () => {
|
|
39
|
+
const original = testSvg;
|
|
40
|
+
const base64 = convertSVGToBase64(original);
|
|
41
|
+
const converted = convertBase64ToSVG(base64);
|
|
42
|
+
// Remove whitespace differences for comparison
|
|
43
|
+
const normalize = (s) => s.replace(/\s+/g, ' ').trim();
|
|
44
|
+
expect(normalize(converted)).toContain('<svg');
|
|
45
|
+
expect(normalize(converted)).toContain('circle');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
//# sourceMappingURL=base64.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base64.test.js","sourceRoot":"","sources":["../../../src/convert/__tests__/base64.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64 encoding/decoding utilities for SVG
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Converts an SVG element or SVG string to a Base64-encoded data URI
|
|
6
|
+
*/
|
|
7
|
+
export declare function convertSVGToBase64(svgContent: Element | string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Converts a Base64-encoded data URI back to an SVG string
|
|
10
|
+
*/
|
|
11
|
+
export declare function convertBase64ToSVG(base64String: string): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64 encoding/decoding utilities for SVG
|
|
3
|
+
*/
|
|
4
|
+
import { serializeSVG } from '../core/element';
|
|
5
|
+
/**
|
|
6
|
+
* Converts an SVG element or SVG string to a Base64-encoded data URI
|
|
7
|
+
*/
|
|
8
|
+
export function convertSVGToBase64(svgContent) {
|
|
9
|
+
const svgString = serializeSVG(svgContent);
|
|
10
|
+
return `data:image/svg+xml;base64,${Buffer.from(svgString).toString('base64')}`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Converts a Base64-encoded data URI back to an SVG string
|
|
14
|
+
*/
|
|
15
|
+
export function convertBase64ToSVG(base64String) {
|
|
16
|
+
try {
|
|
17
|
+
const base64Data = base64String.includes(',')
|
|
18
|
+
? base64String.split(',')[1]
|
|
19
|
+
: base64String;
|
|
20
|
+
// Validate base64 string format
|
|
21
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64Data)) {
|
|
22
|
+
throw new Error('Invalid Base64 string.');
|
|
23
|
+
}
|
|
24
|
+
const svgString = Buffer.from(base64Data, 'base64').toString('utf-8');
|
|
25
|
+
// Basic validation that result looks like SVG
|
|
26
|
+
if (!svgString.includes('<svg') && !svgString.includes('<SVG')) {
|
|
27
|
+
throw new Error('Invalid Base64 string.');
|
|
28
|
+
}
|
|
29
|
+
return svgString;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error instanceof Error && error.message === 'Invalid Base64 string.') {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
throw new Error('Invalid Base64 string.');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=base64.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base64.js","sourceRoot":"","sources":["../../src/convert/base64.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAA4B;IAC7D,MAAM,SAAS,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;IAC3C,OAAO,6BAA6B,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;AAClF,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,YAAoB;IACrD,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC;YAC3C,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC5B,CAAC,CAAC,YAAY,CAAC;QAEjB,gCAAgC;QAChC,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAC/C,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;QAC5C,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAEtE,8CAA8C;QAC9C,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAC/D,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;QAC5C,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,OAAO,KAAK,wBAAwB,EAAE,CAAC;YACzE,MAAM,KAAK,CAAC;QACd,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG to image format conversion utilities
|
|
3
|
+
*/
|
|
4
|
+
import { ConversionOptions } from '../types';
|
|
5
|
+
/**
|
|
6
|
+
* Converts an SVG file to PNG format
|
|
7
|
+
*
|
|
8
|
+
* @param svgPath - Path to the SVG file
|
|
9
|
+
* @param options - Conversion options including scale, format, and quality
|
|
10
|
+
* @returns Promise resolving to a Buffer containing the converted image
|
|
11
|
+
*/
|
|
12
|
+
export declare function svgToImage(svgPath: string, options?: ConversionOptions): Promise<Buffer>;
|
|
13
|
+
/**
|
|
14
|
+
* Converts an SVG file to PNG format (legacy function name)
|
|
15
|
+
*
|
|
16
|
+
* @deprecated Use svgToImage instead
|
|
17
|
+
*/
|
|
18
|
+
export declare function svg2Png(svgPath: string, pngPath?: string, scale?: number): Promise<void | Buffer>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG to image format conversion utilities
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import sharp from 'sharp';
|
|
6
|
+
import { getSVGDimensions } from '../core/dimensions';
|
|
7
|
+
import { validateReadPath, validateWritePath } from '../utils/path-validation';
|
|
8
|
+
/**
|
|
9
|
+
* Converts an SVG file to PNG format
|
|
10
|
+
*
|
|
11
|
+
* @param svgPath - Path to the SVG file
|
|
12
|
+
* @param options - Conversion options including scale, format, and quality
|
|
13
|
+
* @returns Promise resolving to a Buffer containing the converted image
|
|
14
|
+
*/
|
|
15
|
+
export async function svgToImage(svgPath, options = {}) {
|
|
16
|
+
const { scale = 2, format = 'png', quality = 90 } = options;
|
|
17
|
+
// Validate and normalize the file path to prevent path traversal
|
|
18
|
+
const validatedPath = validateReadPath(svgPath, ['.svg']);
|
|
19
|
+
if (!fs.existsSync(validatedPath)) {
|
|
20
|
+
throw new Error(`SVG file not found: ${validatedPath}`);
|
|
21
|
+
}
|
|
22
|
+
const svgContent = fs.readFileSync(validatedPath, 'utf8');
|
|
23
|
+
const dimensions = getSVGDimensions(svgContent);
|
|
24
|
+
if (!dimensions.width || !dimensions.height) {
|
|
25
|
+
throw new Error('SVG must have width and height or viewBox');
|
|
26
|
+
}
|
|
27
|
+
const width = scale * dimensions.width;
|
|
28
|
+
const height = scale * dimensions.height;
|
|
29
|
+
let sharpInstance = sharp(validatedPath).resize(width, height);
|
|
30
|
+
switch (format) {
|
|
31
|
+
case 'jpg':
|
|
32
|
+
case 'jpeg':
|
|
33
|
+
sharpInstance = sharpInstance.jpeg({ quality });
|
|
34
|
+
break;
|
|
35
|
+
case 'webp':
|
|
36
|
+
sharpInstance = sharpInstance.webp({ quality });
|
|
37
|
+
break;
|
|
38
|
+
case 'png':
|
|
39
|
+
default:
|
|
40
|
+
sharpInstance = sharpInstance.png();
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
return sharpInstance.toBuffer();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Converts an SVG file to PNG format (legacy function name)
|
|
47
|
+
*
|
|
48
|
+
* @deprecated Use svgToImage instead
|
|
49
|
+
*/
|
|
50
|
+
export async function svg2Png(svgPath, pngPath, scale) {
|
|
51
|
+
if (!svgPath) {
|
|
52
|
+
throw new Error('SVG file path is required');
|
|
53
|
+
}
|
|
54
|
+
const buffer = await svgToImage(svgPath, { scale: scale ?? 2, format: 'png' });
|
|
55
|
+
if (pngPath) {
|
|
56
|
+
// Validate write path to prevent path traversal
|
|
57
|
+
const validatedWritePath = validateWritePath(pngPath, ['.png']);
|
|
58
|
+
fs.writeFileSync(validatedWritePath, buffer);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
return buffer;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.js","sourceRoot":"","sources":["../../src/convert/image.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE/E;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,OAAe,EACf,UAA6B,EAAE;IAE/B,MAAM,EAAE,KAAK,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,EAAE,OAAO,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC;IAE5D,iEAAiE;IACjE,MAAM,aAAa,GAAG,gBAAgB,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAE1D,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,uBAAuB,aAAa,EAAE,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,UAAU,GAAG,EAAE,CAAC,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;IAC1D,MAAM,UAAU,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAEhD,IAAI,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC/D,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC;IACvC,MAAM,MAAM,GAAG,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC;IAEzC,IAAI,aAAa,GAAG,KAAK,CAAC,aAAa,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAE/D,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,KAAK,CAAC;QACX,KAAK,MAAM;YACT,aAAa,GAAG,aAAa,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;YAChD,MAAM;QACR,KAAK,MAAM;YACT,aAAa,GAAG,aAAa,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;YAChD,MAAM;QACR,KAAK,KAAK,CAAC;QACX;YACE,aAAa,GAAG,aAAa,CAAC,GAAG,EAAE,CAAC;YACpC,MAAM;IACV,CAAC;IAED,OAAO,aAAa,CAAC,QAAQ,EAAE,CAAC;AAClC,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,OAAe,EACf,OAAgB,EAChB,KAAc;IAEd,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAC/C,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAE/E,IAAI,OAAO,EAAE,CAAC;QACZ,gDAAgD;QAChD,MAAM,kBAAkB,GAAG,iBAAiB,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;QAChE,EAAE,CAAC,aAAa,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;QAC7C,OAAO;IACT,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getSVGDimensions } from '../dimensions';
|
|
2
|
+
import { createSVGElement } from '../element';
|
|
3
|
+
describe('SVG Dimensions', () => {
|
|
4
|
+
describe('getSVGDimensions', () => {
|
|
5
|
+
it('should extract dimensions from viewBox', () => {
|
|
6
|
+
const svgString = '<svg viewBox="0 0 100 200"></svg>';
|
|
7
|
+
const dimensions = getSVGDimensions(svgString);
|
|
8
|
+
expect(dimensions.width).toBe(100);
|
|
9
|
+
expect(dimensions.height).toBe(200);
|
|
10
|
+
expect(dimensions.viewBox).toEqual({ x: 0, y: 0, width: 100, height: 200 });
|
|
11
|
+
});
|
|
12
|
+
it('should extract dimensions from width and height attributes', () => {
|
|
13
|
+
const svgString = '<svg width="150" height="250"></svg>';
|
|
14
|
+
const dimensions = getSVGDimensions(svgString);
|
|
15
|
+
expect(dimensions.width).toBe(150);
|
|
16
|
+
expect(dimensions.height).toBe(250);
|
|
17
|
+
});
|
|
18
|
+
it('should prioritize viewBox over width/height', () => {
|
|
19
|
+
const svgString = '<svg viewBox="0 0 100 200" width="150" height="250"></svg>';
|
|
20
|
+
const dimensions = getSVGDimensions(svgString);
|
|
21
|
+
expect(dimensions.width).toBe(100);
|
|
22
|
+
expect(dimensions.height).toBe(200);
|
|
23
|
+
expect(dimensions.viewBox).toEqual({ x: 0, y: 0, width: 100, height: 200 });
|
|
24
|
+
});
|
|
25
|
+
it('should work with SVG element input', () => {
|
|
26
|
+
const svgString = '<svg viewBox="0 0 50 75"></svg>';
|
|
27
|
+
const element = createSVGElement(svgString);
|
|
28
|
+
const dimensions = getSVGDimensions(element);
|
|
29
|
+
expect(dimensions.width).toBe(50);
|
|
30
|
+
expect(dimensions.height).toBe(75);
|
|
31
|
+
});
|
|
32
|
+
it('should handle decimal values', () => {
|
|
33
|
+
const svgString = '<svg viewBox="0 0 100.5 200.75"></svg>';
|
|
34
|
+
const dimensions = getSVGDimensions(svgString);
|
|
35
|
+
expect(dimensions.width).toBe(100.5);
|
|
36
|
+
expect(dimensions.height).toBe(200.75);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
//# sourceMappingURL=dimensions.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dimensions.test.js","sourceRoot":"","sources":["../../../src/core/__tests__/dimensions.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;QAChC,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;YAChD,MAAM,SAAS,GAAG,mCAAmC,CAAC;YACtD,MAAM,UAAU,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;YAE/C,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACnC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACpC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QAC9E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;YACpE,MAAM,SAAS,GAAG,sCAAsC,CAAC;YACzD,MAAM,UAAU,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;YAE/C,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACnC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;YACrD,MAAM,SAAS,GAAG,4DAA4D,CAAC;YAC/E,MAAM,UAAU,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;YAE/C,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACnC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACpC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QAC9E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;YAC5C,MAAM,SAAS,GAAG,iCAAiC,CAAC;YACpD,MAAM,OAAO,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;YAC5C,MAAM,UAAU,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;YAE7C,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAClC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACrC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;YACtC,MAAM,SAAS,GAAG,wCAAwC,CAAC;YAC3D,MAAM,UAAU,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;YAE/C,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createSVGElement, cloneSVGElement, mergeSVGElements, serializeSVG } from '../element';
|
|
2
|
+
import { JSDOM } from 'jsdom';
|
|
3
|
+
describe('Element Manipulation', () => {
|
|
4
|
+
const dom = new JSDOM(`<!DOCTYPE html><html><body></body></html>`);
|
|
5
|
+
const { document } = dom.window;
|
|
6
|
+
describe('createSVGElement', () => {
|
|
7
|
+
it('should create an SVG element from a string', () => {
|
|
8
|
+
const svgString = '<svg><circle cx="50" cy="50" r="40" /></svg>';
|
|
9
|
+
const element = createSVGElement(svgString);
|
|
10
|
+
expect(element.tagName.toLowerCase()).toBe('svg');
|
|
11
|
+
expect(element.querySelector('circle')).toBeTruthy();
|
|
12
|
+
});
|
|
13
|
+
it('should handle complex SVG content', () => {
|
|
14
|
+
const svgString = '<svg viewBox="0 0 100 100"><path d="M10 10 L90 90" /></svg>';
|
|
15
|
+
const element = createSVGElement(svgString);
|
|
16
|
+
expect(element.getAttribute('viewBox')).toBe('0 0 100 100');
|
|
17
|
+
expect(element.querySelector('path')).toBeTruthy();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('cloneSVGElement', () => {
|
|
21
|
+
it('should create a deep clone of an SVG element', () => {
|
|
22
|
+
const svgString = '<svg><circle cx="50" cy="50" r="40" /></svg>';
|
|
23
|
+
const original = createSVGElement(svgString);
|
|
24
|
+
const cloned = cloneSVGElement(original);
|
|
25
|
+
expect(cloned.tagName.toLowerCase()).toBe('svg');
|
|
26
|
+
expect(cloned.querySelector('circle')).toBeTruthy();
|
|
27
|
+
expect(cloned).not.toBe(original);
|
|
28
|
+
});
|
|
29
|
+
it('should preserve all attributes when cloning', () => {
|
|
30
|
+
const svgString = '<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><rect x="10" y="10" width="80" height="80" /></svg>';
|
|
31
|
+
const original = createSVGElement(svgString);
|
|
32
|
+
const cloned = cloneSVGElement(original);
|
|
33
|
+
expect(cloned.getAttribute('viewBox')).toBe('0 0 100 100');
|
|
34
|
+
expect(cloned.querySelector('rect')).toBeTruthy();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('mergeSVGElements', () => {
|
|
38
|
+
it('should merge multiple SVG elements into one', () => {
|
|
39
|
+
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
40
|
+
circle.setAttribute('cx', '50');
|
|
41
|
+
circle.setAttribute('cy', '50');
|
|
42
|
+
circle.setAttribute('r', '40');
|
|
43
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
44
|
+
rect.setAttribute('x', '10');
|
|
45
|
+
rect.setAttribute('y', '10');
|
|
46
|
+
rect.setAttribute('width', '80');
|
|
47
|
+
rect.setAttribute('height', '80');
|
|
48
|
+
const merged = mergeSVGElements([circle, rect]);
|
|
49
|
+
expect(merged.tagName.toLowerCase()).toBe('svg');
|
|
50
|
+
expect(merged.querySelector('circle')).toBeTruthy();
|
|
51
|
+
expect(merged.querySelector('rect')).toBeTruthy();
|
|
52
|
+
});
|
|
53
|
+
it('should handle empty array', () => {
|
|
54
|
+
const merged = mergeSVGElements([]);
|
|
55
|
+
expect(merged.tagName.toLowerCase()).toBe('svg');
|
|
56
|
+
expect(merged.children.length).toBe(0);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('serializeSVG', () => {
|
|
60
|
+
it('should serialize an SVG element to string', () => {
|
|
61
|
+
const svgString = '<svg><circle cx="50" cy="50" r="40" /></svg>';
|
|
62
|
+
const element = createSVGElement(svgString);
|
|
63
|
+
const serialized = serializeSVG(element);
|
|
64
|
+
expect(typeof serialized).toBe('string');
|
|
65
|
+
expect(serialized).toContain('svg');
|
|
66
|
+
expect(serialized).toContain('circle');
|
|
67
|
+
});
|
|
68
|
+
it('should return string as-is if input is already a string', () => {
|
|
69
|
+
const svgString = '<svg><rect /></svg>';
|
|
70
|
+
const serialized = serializeSVG(svgString);
|
|
71
|
+
expect(serialized).toBe(svgString);
|
|
72
|
+
});
|
|
73
|
+
it('should throw error for invalid input', () => {
|
|
74
|
+
expect(() => serializeSVG(null)).toThrow();
|
|
75
|
+
expect(() => serializeSVG(123)).toThrow();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
//# sourceMappingURL=element.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"element.test.js","sourceRoot":"","sources":["../../../src/core/__tests__/element.test.ts"],"names":[],"mappings":"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"}
|