@emasoft/svg-matrix 1.2.1 → 1.3.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/src/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * SVG path conversion, and 2D/3D affine transformations using Decimal.js.
6
6
  *
7
7
  * @module @emasoft/svg-matrix
8
- * @version 1.2.1
8
+ * @version 1.3.0
9
9
  * @license MIT
10
10
  *
11
11
  * @example
@@ -135,7 +135,7 @@ Decimal.set({ precision: 80 });
135
135
  * Library version
136
136
  * @constant {string}
137
137
  */
138
- export const VERSION = '1.2.1';
138
+ export const VERSION = '1.3.0';
139
139
 
140
140
  /**
141
141
  * Default precision for path output (decimal places)
@@ -215,8 +215,8 @@ export function getNodeTypes(element) {
215
215
  const nodeTypes = element.getAttribute("sodipodi:nodetypes");
216
216
  if (!nodeTypes) return null;
217
217
 
218
- // Validate format: should only contain c, s, z, a characters
219
- if (!/^[csza]+$/i.test(nodeTypes)) return null;
218
+ // Validate format: should only contain c, s, z, a characters (case-sensitive)
219
+ if (!/^[csza]+$/.test(nodeTypes)) return null;
220
220
 
221
221
  return nodeTypes;
222
222
  }
@@ -44,8 +44,6 @@
44
44
  */
45
45
 
46
46
  import Decimal from "decimal.js";
47
- import { Matrix as _Matrix } from "./matrix.js";
48
- import * as _Transforms2D from "./transforms2d.js";
49
47
  import * as PolygonClip from "./polygon-clip.js";
50
48
  import * as ClipPathResolver from "./clip-path-resolver.js";
51
49
  import * as MeshGradient from "./mesh-gradient.js";
@@ -1721,10 +1719,15 @@ export function clipWithMeshGradientShape(
1721
1719
  }
1722
1720
 
1723
1721
  // Union all patch polygons to get the complete mesh shape
1724
- let meshShape = meshPolygons[0].polygon;
1725
- if (!meshShape || meshShape.length < 3) {
1722
+ // Validate first polygon before use
1723
+ if (
1724
+ !meshPolygons[0] ||
1725
+ !meshPolygons[0].polygon ||
1726
+ meshPolygons[0].polygon.length < 3
1727
+ ) {
1726
1728
  return [];
1727
1729
  }
1730
+ let meshShape = meshPolygons[0].polygon;
1728
1731
 
1729
1732
  for (let i = 1; i < meshPolygons.length; i++) {
1730
1733
  if (
@@ -1817,10 +1820,15 @@ export function meshGradientToClipPath(meshData, options = {}) {
1817
1820
  }
1818
1821
 
1819
1822
  // Union all patches into one shape
1820
- let result = meshPolygons[0].polygon;
1821
- if (!result || result.length < 3) {
1823
+ // Validate first polygon before use
1824
+ if (
1825
+ !meshPolygons[0] ||
1826
+ !meshPolygons[0].polygon ||
1827
+ meshPolygons[0].polygon.length < 3
1828
+ ) {
1822
1829
  return [];
1823
1830
  }
1831
+ let result = meshPolygons[0].polygon;
1824
1832
 
1825
1833
  for (let i = 1; i < meshPolygons.length; i++) {
1826
1834
  if (!meshPolygons[i] || !meshPolygons[i].polygon) continue; // Skip invalid polygons
@@ -8,8 +8,6 @@
8
8
  */
9
9
 
10
10
  import Decimal from "decimal.js";
11
- import { Matrix as _Matrix } from "./matrix.js";
12
- import * as _Transforms2D from "./transforms2d.js";
13
11
  import * as PolygonClip from "./polygon-clip.js";
14
12
 
15
13
  Decimal.set({ precision: 80 });
@@ -795,7 +795,7 @@ export function pathBoundingBox(pathCommands) {
795
795
  }
796
796
  break;
797
797
 
798
- case "A": // Arc (proper extrema calculation)
798
+ case "A": // Arc (conservative bounding box using extrema bounds)
799
799
  {
800
800
  // Validate required properties (WHY: prevent accessing undefined properties)
801
801
  if (
@@ -825,30 +825,22 @@ export function pathBoundingBox(pathCommands) {
825
825
  maxY = Decimal.max(maxY, currentY, y);
826
826
  samplePoints.push({ x: currentX, y: currentY }, { x, y });
827
827
  } else {
828
- // For proper arc bounding box, include start, end, and potential extrema
829
- // Start and end points
830
- minX = Decimal.min(minX, currentX, x);
831
- minY = Decimal.min(minY, currentY, y);
832
- maxX = Decimal.max(maxX, currentX, x);
833
- maxY = Decimal.max(maxY, currentY, y);
834
-
835
- // For a complete solution, we'd need to:
836
- // 1. Convert to center parameterization (complex for arbitrary precision)
837
- // 2. Check if extrema angles (0°, 90°, 180°, 270°) fall within arc sweep
838
- // 3. Include those extrema in bbox
839
- // For now, conservatively expand bbox by radii to ensure containment
840
- // This may overestimate slightly but guarantees correctness
841
- const centerX = currentX.plus(x).div(2);
842
- const centerY = currentY.plus(y).div(2);
843
- minX = Decimal.min(minX, centerX.minus(rx));
844
- minY = Decimal.min(minY, centerY.minus(ry));
845
- maxX = Decimal.max(maxX, centerX.plus(rx));
846
- maxY = Decimal.max(maxY, centerY.plus(ry));
847
-
848
- // Sample arc for verification points
828
+ // FIX: Use conservative bounds that guarantee containment (WHY: exact arc center requires complex calculations)
829
+ // The tightest conservative bound without full arc parameterization is to
830
+ // consider all possible extrema within a box of size 2*rx by 2*ry centered at each endpoint
831
+ // This ensures we never underestimate the bbox, though we may slightly overestimate
832
+ minX = Decimal.min(minX, currentX, x, currentX.minus(rx), x.minus(rx));
833
+ minY = Decimal.min(minY, currentY, y, currentY.minus(ry), y.minus(ry));
834
+ maxX = Decimal.max(maxX, currentX, x, currentX.plus(rx), x.plus(rx));
835
+ maxY = Decimal.max(maxY, currentY, y, currentY.plus(ry), y.plus(ry));
836
+
837
+ // Sample arc for verification: use parametric sampling along the arc
838
+ // Note: This is still an approximation, but better than linear interpolation
849
839
  const samples = 20;
850
840
  for (let i = 0; i <= samples; i++) {
851
841
  const t = D(i).div(samples);
842
+ // Simple parametric interpolation (approximation)
843
+ // For exact results, we'd need to convert to center parameterization
852
844
  const px = currentX.plus(x.minus(currentX).mul(t));
853
845
  const py = currentY.plus(y.minus(currentY).mul(t));
854
846
  samplePoints.push({ x: px, y: py });
@@ -23,15 +23,10 @@ const EPSILON = new Decimal("1e-40");
23
23
 
24
24
  const {
25
25
  point,
26
- pointsEqual: _pointsEqual,
27
26
  cross,
28
- polygonArea: _polygonArea,
29
27
  polygonIntersection,
30
28
  polygonUnion,
31
29
  polygonDifference,
32
- isCounterClockwise: _isCounterClockwise,
33
- ensureCCW: _ensureCCW,
34
- segmentIntersection: _segmentIntersection,
35
30
  } = PolygonClip;
36
31
 
37
32
  // ============================================================================
@@ -459,6 +459,7 @@ export const colorsProps = new Set([
459
459
 
460
460
  /**
461
461
  * Complete list of known SVG elements
462
+ * Includes both canonical names and lowercase variants for case-insensitive matching
462
463
  */
463
464
  export const knownElements = new Set([
464
465
  // Structural elements
@@ -487,8 +488,13 @@ export const knownElements = new Set([
487
488
  "glyphRef",
488
489
  // Gradient elements
489
490
  "linearGradient",
491
+ "lineargradient", // lowercase variant for case-insensitive matching
490
492
  "radialGradient",
493
+ "radialgradient", // lowercase variant for case-insensitive matching
491
494
  "meshGradient",
495
+ "meshgradient", // lowercase variant for case-insensitive matching
496
+ "meshrow", // SVG 2.0 mesh gradient row
497
+ "meshpatch", // SVG 2.0 mesh gradient patch
492
498
  "stop",
493
499
  // Container elements
494
500
  "a",
@@ -496,6 +502,7 @@ export const knownElements = new Set([
496
502
  "mask",
497
503
  "pattern",
498
504
  "clipPath",
505
+ "clippath", // lowercase variant for case-insensitive matching
499
506
  "switch",
500
507
  // Filter elements
501
508
  "filter",
@@ -531,8 +538,11 @@ export const knownElements = new Set([
531
538
  // Animation elements
532
539
  "animate",
533
540
  "animateColor",
541
+ "animatecolor", // lowercase variant for case-insensitive matching
534
542
  "animateMotion",
543
+ "animatemotion", // lowercase variant for case-insensitive matching
535
544
  "animateTransform",
545
+ "animatetransform", // lowercase variant for case-insensitive matching
536
546
  "set",
537
547
  "mpath",
538
548
  // Other elements
@@ -542,6 +552,7 @@ export const knownElements = new Set([
542
552
  "style",
543
553
  "script",
544
554
  "solidColor",
555
+ "solidcolor", // lowercase variant for case-insensitive matching
545
556
  "hatch",
546
557
  "hatchpath",
547
558
  ]);
@@ -5,7 +5,7 @@
5
5
  * Works in both Node.js and browser environments.
6
6
  *
7
7
  * @module svg-matrix-lib
8
- * @version 1.2.1
8
+ * @version 1.3.0
9
9
  * @license MIT
10
10
  *
11
11
  * @example Browser usage:
@@ -32,7 +32,7 @@ Decimal.set({ precision: 80 });
32
32
  /**
33
33
  * Library version
34
34
  */
35
- export const VERSION = "1.2.1";
35
+ export const VERSION = "1.3.0";
36
36
 
37
37
  // Export core classes
38
38
  export { Decimal, Matrix, Vector };
package/src/svg-parser.js CHANGED
@@ -666,30 +666,6 @@ export class SVGElement {
666
666
  // INTERNAL PARSING FUNCTIONS
667
667
  // ============================================================================
668
668
 
669
- /**
670
- * Check if whitespace should be preserved based on xml:space attribute.
671
- * BUG FIX 2: Helper function to check xml:space="preserve" on element or ancestors
672
- * @private
673
- * @param {SVGElement} element - Element to check for xml:space attribute
674
- * @returns {boolean} True if whitespace should be preserved, false otherwise
675
- */
676
- function _shouldPreserveWhitespace(element) {
677
- // Validation: Ensure element is not null/undefined
678
- if (!element) {
679
- throw new Error(
680
- "_shouldPreserveWhitespace: element cannot be null or undefined",
681
- );
682
- }
683
- let current = element;
684
- while (current) {
685
- const xmlSpace = current.getAttribute("xml:space");
686
- if (xmlSpace === "preserve") return true;
687
- if (xmlSpace === "default") return false;
688
- current = current.parentNode;
689
- }
690
- return false;
691
- }
692
-
693
669
  /**
694
670
  * Parse a single element from SVG string.
695
671
  * @private
@@ -31,10 +31,8 @@ import Decimal from "decimal.js";
31
31
  import {
32
32
  FillRule,
33
33
  pointInPolygonWithRule,
34
- offsetPolygon as _offsetPolygon,
35
34
  strokeToFilledPolygon,
36
35
  } from "./svg-boolean-ops.js";
37
- import * as _PolygonClip from "./polygon-clip.js";
38
36
 
39
37
  Decimal.set({ precision: 80 });
40
38
 
@@ -353,9 +351,9 @@ export class SVGRenderingContext {
353
351
  extent = halfWidth.times(this.strokeMiterlimit);
354
352
  }
355
353
 
356
- // Square linecaps extend by half stroke width beyond endpoints - Why: total extent is stroke radius + cap extension = strokeWidth
354
+ // Square linecaps extend strokeWidth/2 beyond endpoints - Why: total extent is base strokeWidth/2 + cap extension strokeWidth/2 = strokeWidth
357
355
  if (this.strokeLinecap === "square") {
358
- const capExtent = halfWidth.times(2); // strokeWidth/2 + strokeWidth/2
356
+ const capExtent = halfWidth.times(2); // Total extent: strokeWidth (base halfWidth + cap halfWidth)
359
357
  if (capExtent.gt(extent)) {
360
358
  extent = capExtent;
361
359
  }
@@ -5,7 +5,7 @@
5
5
  * Provides 69+ operations for cleaning, optimizing, and transforming SVG files.
6
6
  *
7
7
  * @module svg-toolbox-lib
8
- * @version 1.2.1
8
+ * @version 1.3.0
9
9
  * @license MIT
10
10
  *
11
11
  * @example Browser usage:
@@ -34,7 +34,7 @@ import * as SVGToolboxModule from "./svg-toolbox.js";
34
34
  /**
35
35
  * Library version
36
36
  */
37
- export const VERSION = "1.2.1";
37
+ export const VERSION = "1.3.0";
38
38
 
39
39
  /**
40
40
  * Default export for browser global (window.SVGToolbox)
package/src/svgm-lib.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * comprehensive SVG manipulation (SVGToolbox). Works in Node.js and browser.
6
6
  *
7
7
  * @module svgm-lib
8
- * @version 1.2.1
8
+ * @version 1.3.0
9
9
  * @license MIT
10
10
  *
11
11
  * @example Browser usage:
@@ -49,7 +49,7 @@ Decimal.set({ precision: 80 });
49
49
  /**
50
50
  * Library version
51
51
  */
52
- export const VERSION = "1.2.1";
52
+ export const VERSION = "1.3.0";
53
53
 
54
54
  // Export math classes
55
55
  export { Decimal, Matrix, Vector };
@@ -196,6 +196,97 @@ export function matricesEqual(m1, m2, tolerance = VERIFICATION_TOLERANCE) {
196
196
  return matrixMaxDifference(m1, m2).lessThan(D(tolerance));
197
197
  }
198
198
 
199
+ /**
200
+ * Compute the combined matrix from a list of transform objects.
201
+ * Used internally for verification that optimizations preserve the transform result.
202
+ *
203
+ * @param {Array<Object>} transforms - Array of transform objects with type and params
204
+ * @returns {Matrix} Combined 3x3 transformation matrix
205
+ *
206
+ * @example
207
+ * const transforms = [
208
+ * { type: 'translate', params: { tx: 10, ty: 20 } },
209
+ * { type: 'rotate', params: { angle: 0.5 } }
210
+ * ];
211
+ * const combined = computeCombinedMatrix(transforms);
212
+ */
213
+ export function computeCombinedMatrix(transforms) {
214
+ let combined = identityMatrix();
215
+
216
+ for (const t of transforms) {
217
+ // Validate transform object structure
218
+ if (
219
+ !t ||
220
+ typeof t !== "object" ||
221
+ !t.type ||
222
+ !t.params ||
223
+ typeof t.params !== "object"
224
+ ) {
225
+ continue; // Skip malformed transforms
226
+ }
227
+
228
+ let m = null;
229
+ switch (t.type) {
230
+ case "translate":
231
+ if (
232
+ t.params.tx === null ||
233
+ t.params.tx === undefined ||
234
+ t.params.ty === null ||
235
+ t.params.ty === undefined
236
+ ) {
237
+ continue; // Skip transforms with missing params
238
+ }
239
+ m = translationMatrix(t.params.tx, t.params.ty);
240
+ break;
241
+ case "rotate":
242
+ if (t.params.angle === null || t.params.angle === undefined) {
243
+ continue; // Skip transforms with missing angle
244
+ }
245
+ if (
246
+ t.params.cx !== undefined &&
247
+ t.params.cx !== null &&
248
+ t.params.cy !== undefined &&
249
+ t.params.cy !== null
250
+ ) {
251
+ m = rotationMatrixAroundPoint(
252
+ t.params.angle,
253
+ t.params.cx,
254
+ t.params.cy,
255
+ );
256
+ } else {
257
+ m = rotationMatrix(t.params.angle);
258
+ }
259
+ break;
260
+ case "scale":
261
+ if (
262
+ t.params.sx === null ||
263
+ t.params.sx === undefined ||
264
+ t.params.sy === null ||
265
+ t.params.sy === undefined
266
+ ) {
267
+ continue; // Skip transforms with missing params
268
+ }
269
+ m = scaleMatrix(t.params.sx, t.params.sy);
270
+ break;
271
+ case "matrix":
272
+ if (!t.params.matrix) {
273
+ continue; // Skip transforms with missing matrix
274
+ }
275
+ m = t.params.matrix;
276
+ break;
277
+ default:
278
+ // Skip unknown transform types
279
+ continue;
280
+ }
281
+
282
+ if (m !== null) {
283
+ combined = combined.mul(m);
284
+ }
285
+ }
286
+
287
+ return combined;
288
+ }
289
+
199
290
  // ============================================================================
200
291
  // Transform Merging Functions
201
292
  // ============================================================================
@@ -920,77 +1011,7 @@ export function optimizeTransformList(transforms) {
920
1011
  }
921
1012
 
922
1013
  // Calculate original combined matrix for verification
923
- let originalMatrix = identityMatrix();
924
- for (const t of transforms) {
925
- // Validate transform object structure
926
- if (
927
- !t ||
928
- typeof t !== "object" ||
929
- !t.type ||
930
- !t.params ||
931
- typeof t.params !== "object"
932
- ) {
933
- continue; // Skip malformed transforms
934
- }
935
-
936
- let m = null; // Initialize m to null to catch missing assignments
937
- switch (t.type) {
938
- case "translate":
939
- if (
940
- t.params.tx === null ||
941
- t.params.tx === undefined ||
942
- t.params.ty === null ||
943
- t.params.ty === undefined
944
- ) {
945
- continue; // Skip transforms with missing params
946
- }
947
- m = translationMatrix(t.params.tx, t.params.ty);
948
- break;
949
- case "rotate":
950
- if (t.params.angle === null || t.params.angle === undefined) {
951
- continue; // Skip transforms with missing angle
952
- }
953
- if (
954
- t.params.cx !== undefined &&
955
- t.params.cx !== null &&
956
- t.params.cy !== undefined &&
957
- t.params.cy !== null
958
- ) {
959
- m = rotationMatrixAroundPoint(
960
- t.params.angle,
961
- t.params.cx,
962
- t.params.cy,
963
- );
964
- } else {
965
- m = rotationMatrix(t.params.angle);
966
- }
967
- break;
968
- case "scale":
969
- if (
970
- t.params.sx === null ||
971
- t.params.sx === undefined ||
972
- t.params.sy === null ||
973
- t.params.sy === undefined
974
- ) {
975
- continue; // Skip transforms with missing params
976
- }
977
- m = scaleMatrix(t.params.sx, t.params.sy);
978
- break;
979
- case "matrix":
980
- if (!t.params.matrix) {
981
- continue; // Skip transforms with missing matrix
982
- }
983
- m = t.params.matrix;
984
- break;
985
- default:
986
- // Skip unknown transform types, but don't try to multiply null matrix
987
- continue;
988
- }
989
- // Only multiply if m was successfully assigned (prevents undefined matrix multiplication)
990
- if (m !== null) {
991
- originalMatrix = originalMatrix.mul(m);
992
- }
993
- }
1014
+ const originalMatrix = computeCombinedMatrix(transforms);
994
1015
 
995
1016
  // Step 1: Remove identity transforms
996
1017
  const { transforms: step1, removedCount: _removedCount } =
@@ -1210,77 +1231,7 @@ export function optimizeTransformList(transforms) {
1210
1231
  const { transforms: final } = removeIdentityTransforms(optimized);
1211
1232
 
1212
1233
  // Calculate optimized combined matrix for verification
1213
- let optimizedMatrix = identityMatrix();
1214
- for (const t of final) {
1215
- // Validate transform object structure
1216
- if (
1217
- !t ||
1218
- typeof t !== "object" ||
1219
- !t.type ||
1220
- !t.params ||
1221
- typeof t.params !== "object"
1222
- ) {
1223
- continue; // Skip malformed transforms
1224
- }
1225
-
1226
- let m = null; // Initialize m to null to catch missing assignments
1227
- switch (t.type) {
1228
- case "translate":
1229
- if (
1230
- t.params.tx === null ||
1231
- t.params.tx === undefined ||
1232
- t.params.ty === null ||
1233
- t.params.ty === undefined
1234
- ) {
1235
- continue; // Skip transforms with missing params
1236
- }
1237
- m = translationMatrix(t.params.tx, t.params.ty);
1238
- break;
1239
- case "rotate":
1240
- if (t.params.angle === null || t.params.angle === undefined) {
1241
- continue; // Skip transforms with missing angle
1242
- }
1243
- if (
1244
- t.params.cx !== undefined &&
1245
- t.params.cx !== null &&
1246
- t.params.cy !== undefined &&
1247
- t.params.cy !== null
1248
- ) {
1249
- m = rotationMatrixAroundPoint(
1250
- t.params.angle,
1251
- t.params.cx,
1252
- t.params.cy,
1253
- );
1254
- } else {
1255
- m = rotationMatrix(t.params.angle);
1256
- }
1257
- break;
1258
- case "scale":
1259
- if (
1260
- t.params.sx === null ||
1261
- t.params.sx === undefined ||
1262
- t.params.sy === null ||
1263
- t.params.sy === undefined
1264
- ) {
1265
- continue; // Skip transforms with missing params
1266
- }
1267
- m = scaleMatrix(t.params.sx, t.params.sy);
1268
- break;
1269
- case "matrix":
1270
- if (!t.params.matrix) {
1271
- continue; // Skip transforms with missing matrix
1272
- }
1273
- m = t.params.matrix;
1274
- break;
1275
- default:
1276
- // Skip unknown transform types, but don't try to multiply null matrix
1277
- continue;
1278
- }
1279
- // Only multiply if m was successfully assigned (prevents undefined matrix multiplication)
1280
- if (m !== null) {
1281
- optimizedMatrix = optimizedMatrix.mul(m);
1282
- }
1283
- }
1234
+ const optimizedMatrix = computeCombinedMatrix(final);
1284
1235
 
1285
1236
  // VERIFICATION: Combined matrices must be equal
1286
1237
  const maxError = matrixMaxDifference(originalMatrix, optimizedMatrix);
@@ -15,8 +15,6 @@
15
15
  */
16
16
 
17
17
  import Decimal from "decimal.js";
18
- import { Matrix as _Matrix } from "./matrix.js";
19
- import { Vector as _Vector } from "./vector.js";
20
18
  import * as Transforms2D from "./transforms2d.js";
21
19
 
22
20
  // Use high precision for verifications
@@ -0,0 +1,53 @@
1
+ # SVG Font Replacement Map
2
+ # ========================
3
+ # This file defines font replacements for SVG processing.
4
+ #
5
+ # Format:
6
+ # original_font: replacement_font
7
+ #
8
+ # Examples:
9
+ # "Arial": "Inter" # Replace Arial with Inter
10
+ # "Times New Roman": "Noto Serif"
11
+ #
12
+ # Font sources (in priority order):
13
+ # 1. Local system fonts
14
+ # 2. Google Fonts (default, free)
15
+ # 3. FontGet (npm: fontget)
16
+ # 4. fnt (brew: alexmyczko/fnt/fnt)
17
+ #
18
+ # Options per font:
19
+ # embed: true # Embed as base64 (default: true)
20
+ # subset: true # Only include used glyphs (default: true)
21
+ # source: "google" # Force specific source
22
+ # weight: "400,700" # Specific weights to include
23
+ # style: "normal,italic" # Specific styles
24
+ #
25
+ # Advanced format:
26
+ # "Arial":
27
+ # replacement: "Inter"
28
+ # embed: true
29
+ # subset: true
30
+ # source: "google"
31
+ # weights: ["400", "500", "700"]
32
+
33
+ replacements:
34
+ # Common font replacements
35
+ # "Arial": "Inter"
36
+ # "Helvetica": "Inter"
37
+ # "Times New Roman": "Noto Serif"
38
+ # "Times": "Noto Serif"
39
+ # "Courier New": "Fira Code"
40
+ # "Courier": "Fira Code"
41
+ # "Georgia": "Merriweather"
42
+ # "Verdana": "Open Sans"
43
+ # "Comic Sans MS": "Comic Neue"
44
+
45
+ # Icon fonts
46
+ # "Font Awesome": "Font Awesome 6 Free"
47
+ # "Material Icons": "Material Symbols Outlined"
48
+
49
+ options:
50
+ default_embed: true
51
+ default_subset: true
52
+ fallback_source: "google"
53
+ auto_download: true