@emasoft/svg-matrix 1.0.27 → 1.0.29
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 +325 -0
- package/bin/svg-matrix.js +994 -378
- package/bin/svglinter.cjs +4172 -433
- package/bin/svgm.js +744 -184
- package/package.json +16 -4
- package/src/animation-references.js +71 -52
- package/src/arc-length.js +160 -96
- package/src/bezier-analysis.js +257 -117
- package/src/bezier-intersections.js +411 -148
- package/src/browser-verify.js +240 -100
- package/src/clip-path-resolver.js +350 -142
- package/src/convert-path-data.js +279 -134
- package/src/css-specificity.js +78 -70
- package/src/flatten-pipeline.js +751 -263
- package/src/geometry-to-path.js +511 -182
- package/src/index.js +191 -46
- package/src/inkscape-support.js +404 -0
- package/src/marker-resolver.js +278 -164
- package/src/mask-resolver.js +209 -98
- package/src/matrix.js +147 -67
- package/src/mesh-gradient.js +187 -96
- package/src/off-canvas-detection.js +201 -104
- package/src/path-analysis.js +187 -107
- package/src/path-data-plugins.js +628 -167
- package/src/path-simplification.js +0 -1
- package/src/pattern-resolver.js +125 -88
- package/src/polygon-clip.js +111 -66
- package/src/svg-boolean-ops.js +194 -118
- package/src/svg-collections.js +48 -19
- package/src/svg-flatten.js +282 -164
- package/src/svg-parser.js +427 -200
- package/src/svg-rendering-context.js +147 -104
- package/src/svg-toolbox.js +16411 -3298
- package/src/svg2-polyfills.js +114 -245
- package/src/transform-decomposition.js +46 -41
- package/src/transform-optimization.js +89 -68
- package/src/transforms2d.js +49 -16
- package/src/transforms3d.js +58 -22
- package/src/use-symbol-resolver.js +150 -110
- package/src/vector.js +67 -15
- package/src/vendor/README.md +110 -0
- package/src/vendor/inkscape-hatch-polyfill.js +401 -0
- package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
- package/src/vendor/inkscape-mesh-polyfill.js +843 -0
- package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
- package/src/verification.js +288 -124
package/src/inkscape-support.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* @module inkscape-support
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { SVGElement } from './svg-parser.js';
|
|
11
|
+
|
|
10
12
|
// Inkscape namespace URIs
|
|
11
13
|
export const INKSCAPE_NS = 'http://www.inkscape.org/namespaces/inkscape';
|
|
12
14
|
export const SODIPODI_NS = 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd';
|
|
@@ -23,6 +25,8 @@ export const INKSCAPE_PREFIXES = ['inkscape', 'sodipodi'];
|
|
|
23
25
|
*/
|
|
24
26
|
export function isInkscapeLayer(element) {
|
|
25
27
|
if (!element || element.tagName !== 'g') return false;
|
|
28
|
+
// Safety check: ensure getAttribute method exists before calling it
|
|
29
|
+
if (typeof element.getAttribute !== 'function') return false;
|
|
26
30
|
return element.getAttribute('inkscape:groupmode') === 'layer';
|
|
27
31
|
}
|
|
28
32
|
|
|
@@ -221,7 +225,9 @@ export function getTiledCloneSource(element) {
|
|
|
221
225
|
* @returns {boolean} True if Inkscape namespaces are present
|
|
222
226
|
*/
|
|
223
227
|
export function hasInkscapeNamespaces(doc) {
|
|
228
|
+
if (!doc) return false;
|
|
224
229
|
const svg = doc.documentElement || doc;
|
|
230
|
+
if (!svg || typeof svg.getAttribute !== 'function') return false;
|
|
225
231
|
const hasInkscape = svg.getAttribute('xmlns:inkscape') === INKSCAPE_NS;
|
|
226
232
|
const hasSodipodi = svg.getAttribute('xmlns:sodipodi') === SODIPODI_NS;
|
|
227
233
|
return hasInkscape || hasSodipodi;
|
|
@@ -237,6 +243,11 @@ export function hasInkscapeNamespaces(doc) {
|
|
|
237
243
|
export function ensureInkscapeNamespaces(doc) {
|
|
238
244
|
const svg = doc.documentElement || doc;
|
|
239
245
|
|
|
246
|
+
// Safety check: ensure getAttribute and setAttribute methods exist
|
|
247
|
+
if (typeof svg.getAttribute !== 'function' || typeof svg.setAttribute !== 'function') {
|
|
248
|
+
return doc;
|
|
249
|
+
}
|
|
250
|
+
|
|
240
251
|
if (!svg.getAttribute('xmlns:inkscape')) {
|
|
241
252
|
svg.setAttribute('xmlns:inkscape', INKSCAPE_NS);
|
|
242
253
|
}
|
|
@@ -246,3 +257,396 @@ export function ensureInkscapeNamespaces(doc) {
|
|
|
246
257
|
|
|
247
258
|
return doc;
|
|
248
259
|
}
|
|
260
|
+
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// LAYER EXTRACTION
|
|
263
|
+
// ============================================================================
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Find all IDs referenced by an element and its descendants.
|
|
267
|
+
* Looks for url(#id) references in fill, stroke, clip-path, mask, marker-*, filter, etc.
|
|
268
|
+
* Also checks xlink:href and href attributes for #id references.
|
|
269
|
+
*
|
|
270
|
+
* @param {Object} element - SVG element to scan
|
|
271
|
+
* @returns {Set<string>} Set of referenced IDs
|
|
272
|
+
*/
|
|
273
|
+
export function findReferencedIds(element) {
|
|
274
|
+
const ids = new Set();
|
|
275
|
+
|
|
276
|
+
// Attributes that can contain url(#id) references
|
|
277
|
+
const urlRefAttrs = [
|
|
278
|
+
'fill', 'stroke', 'clip-path', 'mask', 'filter',
|
|
279
|
+
'marker-start', 'marker-mid', 'marker-end',
|
|
280
|
+
'fill-opacity', 'stroke-opacity' // Sometimes reference paint servers
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
// Attributes that can contain #id or url(#id) references
|
|
284
|
+
const hrefAttrs = ['href', 'xlink:href'];
|
|
285
|
+
|
|
286
|
+
const extractUrlId = (value) => {
|
|
287
|
+
if (!value) return null;
|
|
288
|
+
// Match url(#id) or url("#id")
|
|
289
|
+
const match = value.match(/url\(["']?#([^"')]+)["']?\)/);
|
|
290
|
+
return match ? match[1] : null;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const extractHrefId = (value) => {
|
|
294
|
+
if (!value) return null;
|
|
295
|
+
// Match #id references
|
|
296
|
+
if (value.startsWith('#')) {
|
|
297
|
+
return value.slice(1);
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const walk = (el) => {
|
|
303
|
+
if (!el) return;
|
|
304
|
+
|
|
305
|
+
// Check url() references
|
|
306
|
+
for (const attr of urlRefAttrs) {
|
|
307
|
+
const id = extractUrlId(el.getAttribute?.(attr));
|
|
308
|
+
if (id) ids.add(id);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Check href references
|
|
312
|
+
for (const attr of hrefAttrs) {
|
|
313
|
+
const id = extractHrefId(el.getAttribute?.(attr));
|
|
314
|
+
if (id) ids.add(id);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Check style attribute for url() references
|
|
318
|
+
const style = el.getAttribute?.('style');
|
|
319
|
+
if (style) {
|
|
320
|
+
const urlMatches = style.matchAll(/url\(["']?#([^"')]+)["']?\)/g);
|
|
321
|
+
for (const match of urlMatches) {
|
|
322
|
+
ids.add(match[1]);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Recurse into children
|
|
327
|
+
if (el.children) {
|
|
328
|
+
for (const child of el.children) {
|
|
329
|
+
walk(child);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
walk(element);
|
|
335
|
+
return ids;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Build a map of all defs elements by their ID.
|
|
340
|
+
*
|
|
341
|
+
* @param {Object} doc - Parsed SVG document
|
|
342
|
+
* @returns {Map<string, Object>} Map of ID to element
|
|
343
|
+
*/
|
|
344
|
+
export function buildDefsMapFromDefs(doc) {
|
|
345
|
+
const defsMap = new Map();
|
|
346
|
+
|
|
347
|
+
const walk = (el) => {
|
|
348
|
+
if (!el) return;
|
|
349
|
+
|
|
350
|
+
// If element has an ID, add to map
|
|
351
|
+
const id = el.getAttribute?.('id');
|
|
352
|
+
if (id) {
|
|
353
|
+
defsMap.set(id, el);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Recurse
|
|
357
|
+
if (el.children) {
|
|
358
|
+
for (const child of el.children) {
|
|
359
|
+
walk(child);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// Only scan defs elements for efficiency
|
|
365
|
+
const findDefs = (el) => {
|
|
366
|
+
if (!el) return;
|
|
367
|
+
if (el.tagName === 'defs') {
|
|
368
|
+
walk(el);
|
|
369
|
+
}
|
|
370
|
+
if (el.children) {
|
|
371
|
+
for (const child of el.children) {
|
|
372
|
+
findDefs(child);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
findDefs(doc);
|
|
378
|
+
return defsMap;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Recursively resolve all dependencies for a set of IDs.
|
|
383
|
+
* Defs elements can reference other defs (e.g., gradient with xlink:href to another gradient).
|
|
384
|
+
*
|
|
385
|
+
* @param {Set<string>} initialIds - Initial set of IDs to resolve
|
|
386
|
+
* @param {Map<string, Object>} defsMap - Map of all defs elements
|
|
387
|
+
* @returns {Set<string>} Complete set of IDs including all nested dependencies
|
|
388
|
+
*/
|
|
389
|
+
export function resolveDefsDependencies(initialIds, defsMap) {
|
|
390
|
+
const resolved = new Set();
|
|
391
|
+
const toProcess = [...initialIds];
|
|
392
|
+
|
|
393
|
+
while (toProcess.length > 0) {
|
|
394
|
+
const id = toProcess.pop();
|
|
395
|
+
if (resolved.has(id)) continue;
|
|
396
|
+
|
|
397
|
+
const element = defsMap.get(id);
|
|
398
|
+
if (!element) continue;
|
|
399
|
+
|
|
400
|
+
resolved.add(id);
|
|
401
|
+
|
|
402
|
+
// Find references within this def element
|
|
403
|
+
const nestedRefs = findReferencedIds(element);
|
|
404
|
+
for (const nestedId of nestedRefs) {
|
|
405
|
+
if (!resolved.has(nestedId) && defsMap.has(nestedId)) {
|
|
406
|
+
toProcess.push(nestedId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return resolved;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Deep clone an SVG element and all its children using proper SVGElement class.
|
|
416
|
+
*
|
|
417
|
+
* @param {Object} element - Element to clone
|
|
418
|
+
* @returns {SVGElement} Cloned element with serialize() method
|
|
419
|
+
*/
|
|
420
|
+
export function cloneElement(element) {
|
|
421
|
+
if (!element) return null;
|
|
422
|
+
|
|
423
|
+
// Get attributes as plain object
|
|
424
|
+
const attrs = {};
|
|
425
|
+
if (element._attributes) {
|
|
426
|
+
Object.assign(attrs, element._attributes);
|
|
427
|
+
} else if (element.getAttributeNames) {
|
|
428
|
+
for (const name of element.getAttributeNames()) {
|
|
429
|
+
attrs[name] = element.getAttribute(name);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Clone children recursively
|
|
434
|
+
const clonedChildren = [];
|
|
435
|
+
if (element.children) {
|
|
436
|
+
for (const child of element.children) {
|
|
437
|
+
const clonedChild = cloneElement(child);
|
|
438
|
+
if (clonedChild) {
|
|
439
|
+
clonedChildren.push(clonedChild);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Create proper SVGElement with serialize() method
|
|
445
|
+
const clone = new SVGElement(
|
|
446
|
+
element.tagName,
|
|
447
|
+
attrs,
|
|
448
|
+
clonedChildren,
|
|
449
|
+
element.textContent || null
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
return clone;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Extract a single layer as a standalone SVG document.
|
|
457
|
+
* Includes only the defs elements that are referenced by the layer.
|
|
458
|
+
*
|
|
459
|
+
* @param {Object} doc - Source parsed SVG document
|
|
460
|
+
* @param {Object|string} layerOrId - Layer element or layer ID to extract
|
|
461
|
+
* @param {Object} [options] - Options
|
|
462
|
+
* @param {boolean} [options.includeHiddenLayers=false] - Include hidden layers in output
|
|
463
|
+
* @param {boolean} [options.preserveTransform=true] - Preserve layer transform attribute
|
|
464
|
+
* @returns {{svg: SVGElement, layerInfo: {id: string, label: string}}} Extracted SVG and layer info
|
|
465
|
+
*/
|
|
466
|
+
export function extractLayer(doc, layerOrId, options = {}) {
|
|
467
|
+
const { preserveTransform = true } = options;
|
|
468
|
+
|
|
469
|
+
// Find the layer element
|
|
470
|
+
let layer;
|
|
471
|
+
if (typeof layerOrId === 'string') {
|
|
472
|
+
const layers = findLayers(doc);
|
|
473
|
+
const found = layers.find(l => l.id === layerOrId || l.label === layerOrId);
|
|
474
|
+
if (!found || !found.element) {
|
|
475
|
+
throw new Error(`Layer not found or invalid: ${layerOrId}`);
|
|
476
|
+
}
|
|
477
|
+
layer = found.element;
|
|
478
|
+
} else {
|
|
479
|
+
layer = layerOrId;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (!isInkscapeLayer(layer)) {
|
|
483
|
+
throw new Error('Element is not an Inkscape layer');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Get SVG root element
|
|
487
|
+
const svgRoot = doc.documentElement || doc;
|
|
488
|
+
|
|
489
|
+
// Build defs map from source document
|
|
490
|
+
const defsMap = buildDefsMapFromDefs(doc);
|
|
491
|
+
|
|
492
|
+
// Find all IDs referenced by this layer
|
|
493
|
+
const referencedIds = findReferencedIds(layer);
|
|
494
|
+
|
|
495
|
+
// Resolve all nested dependencies
|
|
496
|
+
const requiredDefIds = resolveDefsDependencies(referencedIds, defsMap);
|
|
497
|
+
|
|
498
|
+
// Get SVG root attributes
|
|
499
|
+
const svgAttrs = {};
|
|
500
|
+
if (svgRoot._attributes) {
|
|
501
|
+
Object.assign(svgAttrs, svgRoot._attributes);
|
|
502
|
+
} else if (svgRoot.getAttributeNames) {
|
|
503
|
+
for (const name of svgRoot.getAttributeNames()) {
|
|
504
|
+
svgAttrs[name] = svgRoot.getAttribute(name);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Build children array for new SVG
|
|
509
|
+
const svgChildren = [];
|
|
510
|
+
|
|
511
|
+
// Create defs element with required definitions
|
|
512
|
+
if (requiredDefIds.size > 0) {
|
|
513
|
+
const defsChildren = [];
|
|
514
|
+
for (const id of requiredDefIds) {
|
|
515
|
+
const defElement = defsMap.get(id);
|
|
516
|
+
if (defElement) {
|
|
517
|
+
defsChildren.push(cloneElement(defElement));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (defsChildren.length > 0) {
|
|
521
|
+
const newDefs = new SVGElement('defs', {}, defsChildren, null);
|
|
522
|
+
svgChildren.push(newDefs);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Clone the layer
|
|
527
|
+
const clonedLayer = cloneElement(layer);
|
|
528
|
+
|
|
529
|
+
// Optionally remove the transform
|
|
530
|
+
if (!preserveTransform && clonedLayer._attributes) {
|
|
531
|
+
delete clonedLayer._attributes.transform;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
svgChildren.push(clonedLayer);
|
|
535
|
+
|
|
536
|
+
// Create new SVG document using SVGElement
|
|
537
|
+
const newSvg = new SVGElement('svg', svgAttrs, svgChildren, null);
|
|
538
|
+
|
|
539
|
+
// Get layer info
|
|
540
|
+
const layerInfo = {
|
|
541
|
+
id: layer.getAttribute('id'),
|
|
542
|
+
label: getLayerLabel(layer)
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
return { svg: newSvg, layerInfo };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Extract all layers from an Inkscape SVG as separate documents.
|
|
550
|
+
*
|
|
551
|
+
* @param {Object} doc - Source parsed SVG document
|
|
552
|
+
* @param {Object} [options] - Options
|
|
553
|
+
* @param {boolean} [options.includeHidden=false] - Include hidden layers (display:none or visibility:hidden)
|
|
554
|
+
* @param {boolean} [options.preserveTransform=true] - Preserve layer transform attributes
|
|
555
|
+
* @returns {Array<{svg: Object, layerInfo: {id: string, label: string}}>} Array of extracted SVGs
|
|
556
|
+
*/
|
|
557
|
+
export function extractAllLayers(doc, options = {}) {
|
|
558
|
+
const { includeHidden = false } = options;
|
|
559
|
+
const layers = findLayers(doc);
|
|
560
|
+
const results = [];
|
|
561
|
+
|
|
562
|
+
for (const layerData of layers) {
|
|
563
|
+
const layer = layerData.element;
|
|
564
|
+
|
|
565
|
+
// Skip hidden layers unless requested
|
|
566
|
+
if (!includeHidden) {
|
|
567
|
+
const style = layer.getAttribute('style') || '';
|
|
568
|
+
const display = layer.getAttribute('display');
|
|
569
|
+
const visibility = layer.getAttribute('visibility');
|
|
570
|
+
|
|
571
|
+
if (display === 'none' ||
|
|
572
|
+
visibility === 'hidden' ||
|
|
573
|
+
style.includes('display:none') ||
|
|
574
|
+
style.includes('visibility:hidden')) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
const extracted = extractLayer(doc, layer, options);
|
|
581
|
+
results.push(extracted);
|
|
582
|
+
} catch (e) {
|
|
583
|
+
// Skip layers that fail to extract
|
|
584
|
+
console.warn(`Failed to extract layer ${layerData.id || layerData.label}: ${e.message}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return results;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Get a summary of shared resources between layers.
|
|
593
|
+
* Useful for understanding what defs are shared across layers.
|
|
594
|
+
*
|
|
595
|
+
* @param {Object} doc - Parsed SVG document
|
|
596
|
+
* @returns {Object} Summary of shared resources
|
|
597
|
+
*/
|
|
598
|
+
export function analyzeLayerDependencies(doc) {
|
|
599
|
+
const layers = findLayers(doc);
|
|
600
|
+
const defsMap = buildDefsMapFromDefs(doc);
|
|
601
|
+
const layerRefs = new Map(); // layer ID -> Set of referenced def IDs
|
|
602
|
+
const defUsage = new Map(); // def ID -> Set of layer IDs that use it
|
|
603
|
+
|
|
604
|
+
for (const layerData of layers) {
|
|
605
|
+
const layer = layerData.element;
|
|
606
|
+
const layerId = layerData.id || layerData.label || 'unnamed';
|
|
607
|
+
|
|
608
|
+
// Find refs for this layer
|
|
609
|
+
const refs = findReferencedIds(layer);
|
|
610
|
+
const resolved = resolveDefsDependencies(refs, defsMap);
|
|
611
|
+
|
|
612
|
+
layerRefs.set(layerId, resolved);
|
|
613
|
+
|
|
614
|
+
// Track which defs are used by which layers
|
|
615
|
+
for (const defId of resolved) {
|
|
616
|
+
if (!defUsage.has(defId)) {
|
|
617
|
+
defUsage.set(defId, new Set());
|
|
618
|
+
}
|
|
619
|
+
defUsage.get(defId).add(layerId);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Find shared defs (used by more than one layer)
|
|
624
|
+
const sharedDefs = [];
|
|
625
|
+
const exclusiveDefs = new Map(); // layer ID -> defs only used by that layer
|
|
626
|
+
|
|
627
|
+
for (const [defId, layerSet] of defUsage) {
|
|
628
|
+
if (layerSet.size > 1) {
|
|
629
|
+
sharedDefs.push({
|
|
630
|
+
id: defId,
|
|
631
|
+
usedBy: [...layerSet]
|
|
632
|
+
});
|
|
633
|
+
} else {
|
|
634
|
+
const layerId = [...layerSet][0];
|
|
635
|
+
if (!exclusiveDefs.has(layerId)) {
|
|
636
|
+
exclusiveDefs.set(layerId, []);
|
|
637
|
+
}
|
|
638
|
+
exclusiveDefs.get(layerId).push(defId);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
layers: layers.map(l => ({
|
|
644
|
+
id: l.id,
|
|
645
|
+
label: l.label,
|
|
646
|
+
referencedDefs: [...(layerRefs.get(l.id || l.label || 'unnamed') || [])]
|
|
647
|
+
})),
|
|
648
|
+
sharedDefs,
|
|
649
|
+
exclusiveDefs: Object.fromEntries(exclusiveDefs),
|
|
650
|
+
totalDefs: defsMap.size
|
|
651
|
+
};
|
|
652
|
+
}
|