svg-vectordrawable 0.1.1 → 0.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 CHANGED
@@ -12,7 +12,7 @@ npm i svg-vectordrawable
12
12
 
13
13
  ## Why another one?
14
14
 
15
- `svg2vectordrawable` is unmaintained (last release 2022), pins `svgo@2`, and ignores
15
+ `svg2vectordrawable` is unmaintained (last release 2022), depends on `svgo@^2.8`, and ignores
16
16
  `gradientTransform` (Figma radial gradients render at the wrong place). This library is on
17
17
  `svgo@4`, bakes `gradientTransform` into the Android coordinates, and **fails loud** instead of
18
18
  emitting plausible‑but‑wrong output.
@@ -22,19 +22,74 @@ emitting plausible‑but‑wrong output.
22
22
  ```ts
23
23
  import { convert } from 'svg-vectordrawable';
24
24
 
25
- const { xml, warnings } = convert(svgString, {
25
+ const { xml, warnings, minSdk } = convert(svgString, {
26
26
  optimize: true, // run svgo normalization first (recommended)
27
27
  currentColor: '#000', // value substituted for `currentColor`
28
28
  floatPrecision: 3,
29
29
  fillBlackForUnfilled: true, // unfilled paths get black (SVG default)
30
30
  xmlTag: false, // prepend <?xml ...?>
31
31
  tint: '#FFFFFFFF', // android:tint on <vector> (Android color literal)
32
- strict: false, // throw on unsupported constructs instead of warning
32
+ strict: false, // true: throw on lossy and approximated output; 'lossy': only on lossy output
33
+ minSdk: 21, // warn (`min-sdk-exceeded`) when the output needs a higher API level
34
+ rules: {}, // per-code severity, overrides `strict` (see below)
33
35
  onWarn: (w) => console.warn(w.code, w.message),
34
36
  });
35
37
  ```
36
38
 
37
- `convert` is synchronous and returns `{ xml, warnings }`.
39
+ `convert` is synchronous and returns `{ xml, warnings, minSdk }`. `minSdk` is the Android API level the
40
+ output needs: 24 when it uses gradients or `fillType="evenOdd"`, otherwise 21.
41
+
42
+ ### Preview
43
+
44
+ `vectorDrawableToSvg` turns a VectorDrawable back into SVG, to preview the result in a browser or a PR:
45
+
46
+ ```ts
47
+ import { convert, vectorDrawableToSvg } from 'svg-vectordrawable';
48
+ const preview = vectorDrawableToSvg(convert(svg).xml, { density: 2, applyTint: true });
49
+ ```
50
+
51
+ ### Warnings, errors and `rules`
52
+
53
+ Every construct that cannot be converted faithfully emits a warning with a stable `code` (listed in
54
+ the exported `WARNING_CODES`). Each code has a category (exported `WARNING_CATEGORIES`): **lossy**
55
+ (content lost or wrong), **approximation** (rendering slightly differs) or **info** (Android lint
56
+ style, API level). `strict: true` turns lossy and approximation warnings into errors, `strict: 'lossy'`
57
+ only lossy ones; info warnings are never raised by `strict`. `rules` sets the severity per code
58
+ (`'off' | 'warn' | 'error'`) and wins over `strict`. Errors are thrown as a
59
+ `ConversionError` carrying the offending `warning`, so you can fall back (e.g. to a PNG):
60
+
61
+ ```ts
62
+ import { convert, ConversionError } from 'svg-vectordrawable';
63
+
64
+ try {
65
+ // Reject anything lossy, but accept approximated group opacity.
66
+ const { xml } = convert(svg, { strict: true, rules: { 'opacity-approximated': 'warn' } });
67
+ } catch (err) {
68
+ if (err instanceof ConversionError) console.log('cannot convert:', err.warning.code);
69
+ else throw err;
70
+ }
71
+ ```
72
+
73
+ | Code | Meaning |
74
+ | ------------------------------ | -------------------------------------------------------------------------------------------- |
75
+ | `unsupported-element` | `<text>`, `<image>`, `<foreignObject>`, `<switch>` — skipped |
76
+ | `unsupported-attribute` | non-clip `mask`, `filter`, `marker*`, `vector-effect`, `paint-order`, `arcs` joins — ignored |
77
+ | `unsupported-stroke-dasharray` | dash pattern that cannot be resolved (`em`/`ex` units) — drawn solid |
78
+ | `unsupported-stroke-gradient` | no longer emitted (gradient strokes are supported); kept for compatibility |
79
+ | `unsupported-paint` | `<pattern>` paint — fallback color or black |
80
+ | `unsupported-style` | CSS left in a `<style>` that could not be applied to elements — ignored |
81
+ | `unsupported-clip-path` | `objectBoundingBox` clip, evenodd clip with crossing contours, unconvertible content |
82
+ | `gradient-approximated` | radial focal point; elliptical radial on a path that also has a solid stroke |
83
+ | `opacity-approximated` | opacity folded onto overlapping children / fill+stroke — overlaps look darker |
84
+ | `missing-gradient` | `url(#id)` paint pointing at nothing, no fallback — black |
85
+ | `missing-clip-path` | `clip-path` pointing at nothing — ignored |
86
+ | `group-skew` | skewed `<g>` baked into path geometry |
87
+ | `gradient-under-skew` | gradient placement under a baked skew is approximate |
88
+ | `gradient-bbox-unavailable` | `objectBoundingBox` gradient without a measurable path box |
89
+ | `empty-path` | shape without geometry — skipped |
90
+ | `min-sdk-exceeded` (info) | the output needs a higher API level than the `minSdk` option |
91
+ | `large-vector` (info) | `android:width`/`height` above 200 dp (Android lint `VectorRaster`) |
92
+ | `long-path-data` (info, off) | a `pathData` longer than 800 characters (lint `VectorPath`); enable it with `rules` |
38
93
 
39
94
  Node file helpers:
40
95
 
@@ -42,6 +97,7 @@ Node file helpers:
42
97
  import { convertFile, convertDir } from 'svg-vectordrawable';
43
98
  convertFile('icon.svg', 'res/drawable/icon.xml');
44
99
  convertDir('svg/', 'res/drawable/');
100
+ convertDir('svg/', 'res/drawable/', {}, { androidNames: true }); // valid Android resource names
45
101
  ```
46
102
 
47
103
  ### CLI
@@ -53,6 +109,10 @@ svgvd icon.svg --stdout # print to stdout
53
109
  svgvd -s '<svg>…</svg>' # convert an inline SVG string
54
110
  svgvd icon.svg --xml-tag --tint '#FFFFFFFF'
55
111
  svgvd icon.svg --strict # fail on anything not representable
112
+ svgvd icon.svg --strict --rule opacity-approximated=warn # per-code severity
113
+ svgvd icon.svg --strict=lossy # fail only when content would be lost
114
+ svgvd icon.svg --min-sdk 21 # warn when the output needs a higher API level
115
+ svgvd icons/ -o res/drawable/ --android-names # Arrow-Left.svg → arrow_left.xml (valid resource names)
56
116
  ```
57
117
 
58
118
  ### Browser
@@ -65,39 +125,67 @@ import { convert } from 'svg-vectordrawable/browser';
65
125
  const { xml } = convert(svgString);
66
126
  ```
67
127
 
128
+ `svg-vectordrawable/browser-lite` is the same API without svgo (116 KB instead of 1.1 MB for the
129
+ IIFE build): conversion always runs with `optimize: false`, so `<style>` CSS is not inlined and
130
+ shapes are converted as authored.
131
+
68
132
  ## What it handles
69
133
 
70
- | Feature | Status |
71
- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
72
- | Paths, `fill`, `stroke` (width/cap/join/miter), `fill-rule` → `fillType` | ✅ |
73
- | Shapes (`rect`/`circle`/`ellipse`/`line`/`poly*`) → path | ✅ |
74
- | Colors: `#rgb[a]`, `#rrggbb[aa]`, `rgb()/rgba()`, `hsl()/hsla()`, named, `currentColor` | ✅ |
75
- | Attribute inheritance (incl. presentation attrs on the `<svg>` root and `<g>`) | ✅ |
76
- | Inline `style="…"` and `<style>` (via svgo) | ✅ |
77
- | Linear & radial **gradients**, `gradientTransform`, **`objectBoundingBox`** (via path bbox), `href` sharing, `spreadMethod` → `tileMode` | ✅ |
78
- | `<g transform>` → `<group>` (translate/rotate/scale); **skew/shear baked into geometry** | ✅ |
79
- | **`<use>` / `<symbol>`** references — inlined before conversion | ✅ |
80
- | `clip-path` → `<clip-path>` | ✅ (basic) |
81
- | `opacity` folded into `fillAlpha`/`strokeAlpha` | ✅ |
134
+ | Feature | Status |
135
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
136
+ | Paths, `fill`, `stroke` (width/cap/join/miter), `fill-rule` → `fillType` | ✅ |
137
+ | Shapes (`rect`/`circle`/`ellipse`/`line`/`poly*`) → path | ✅ |
138
+ | Colors: `#rgb[a]`, `#rrggbb[aa]`, `rgb()/rgba()`, `hsl()/hsla()`, named, `currentColor` | ✅ |
139
+ | Attribute inheritance (incl. presentation attrs on the `<svg>` root and `<g>`) | ✅ |
140
+ | Inline `style="…"` and `<style>` (via svgo) | ✅ |
141
+ | Linear & radial **gradients**, `gradientTransform`, **`objectBoundingBox`** (via path bbox), `href` sharing, `spreadMethod` → `tileMode` | ✅ |
142
+ | `<g transform>` → `<group>` (translate/rotate/scale); **skew/shear baked into geometry** | ✅ |
143
+ | **`<use>` / `<symbol>`** references — inlined before conversion | ✅ |
144
+ | `clip-path` → `<clip-path>` (on shapes and `<g>`, incl. `<use>` / transformed clip content) | ✅ |
145
+ | `opacity` folded into `fillAlpha`/`strokeAlpha` (root `<svg>` opacity → exact `android:alpha`) | ✅ |
146
+ | `viewBox` origin, `preserveAspectRatio` (meet / slice / none), nested `<svg>`, `<a>`, `display` / `visibility`, units (`px`/`in`/`cm`/`mm`/`pt`/`pc`/`%`) | ✅ |
82
147
 
83
148
  ## Known limitations
84
149
 
85
150
  A VectorDrawable simply cannot represent some SVG features. These are **warned** (or throw in
86
151
  `strict` mode), never silently mis‑rendered:
87
152
 
88
- - `<mask>`, `<filter>`, `<pattern>`, `<image>`, `<text>` — not representable.
89
- - `stroke-dasharray` and gradient **strokes** — dropped (VectorDrawable supports neither).
153
+ - `<filter>`, `<pattern>`, `<image>`, `<text>` — not representable. A `<mask>` is converted to a
154
+ `<clip-path>` when it is clip-equivalent (opaque white content, or opaque content with `mask-type: alpha`);
155
+ any other mask (gray, translucent, stroked, gradient…) is warned with the reason.
156
+ - `stroke-dasharray` in font-relative units (`em`, `ex`) — the stroke is drawn solid. Other dash
157
+ patterns are baked into the path geometry (VectorDrawable has no native dashes).
158
+ - Radial gradients are circles in VectorDrawable. Elliptical radials (non-uniform `gradientTransform`,
159
+ `objectBoundingBox` on a non-square shape) are made exact by drawing the fill inside a `<group>`
160
+ carrying the ellipse's deformation; they stay approximated only when the same path also has a solid
161
+ stroke (it would be distorted). Focal points (`fx`/`fy`) are approximated (`gradient-approximated`).
162
+ - CSS that svgo cannot inline (`@media`, `:hover`, complex selectors) — `unsupported-style`.
90
163
  - `gradientUnits="objectBoundingBox"` is resolved from the path's bounding box; only when that box
91
164
  is unavailable does it fall back to the viewport (with a warning).
92
- - `clip-path` on a `<g>` (vs. on a drawable element) is not applied.
165
+ - `filter`, `marker-*`, non-clip `mask` references and `clipPathUnits="objectBoundingBox"` — warned, not applied.
166
+ - Group `opacity` has no VectorDrawable equivalent: it is folded onto each child, which is exact only
167
+ when the children do not overlap (`opacity-approximated` otherwise).
93
168
 
94
169
  ## Robustness
95
170
 
96
- Validated against ~10,700 real icons (Feather, Bootstrap Icons, Heroicons, Tabler) plus ~900
97
- Figma‑exported brand assets: 100% converted with the correct paint model (stroke icons stay
98
- strokes, filled icons stay fills). A sample (incl. gradients, `gradientTransform`, `objectBoundingBox`,
99
- `<use>`, sheared groups, clip-path) is **compiled with `aapt2`** — Android's own toolchain accepts
100
- the output — and a golden-snapshot suite locks the exact XML against regressions.
171
+ - **Icon corpus in CI** (`scripts/corpus.mjs`): 9,251 icons (Feather, Bootstrap Icons, Heroicons,
172
+ Tabler, pinned versions) converted with and without svgo on every push — zero exceptions, zero
173
+ warnings required.
174
+ - **Android toolchain**: the fixtures (gradients, `gradientTransform`, `objectBoundingBox`, `<use>`,
175
+ sheared groups, clip-path, nested transforms) are **compiled and linked with `aapt2`** against
176
+ `android.jar`, so attribute names and values are validated, not just XML well-formedness.
177
+ - **Visual regression**: every fixture is rendered with resvg next to its VectorDrawable and must match
178
+ within 1% of inked pixels (`pnpm test:visual`).
179
+ - **Android's own renderer**: the generated VectorDrawables are drawn by layoutlib (Android's Skia/hwui,
180
+ through Paparazzi on the JVM) and compared with the source SVG (`pnpm run validate:android`; needs a
181
+ full JDK 17+ and the Android SDK).
182
+ - **Golden snapshots** lock the exact XML against regressions.
183
+
184
+ ## Documentation
185
+
186
+ - [Migrating from `svg2vectordrawable`](docs/migration-from-svg2vectordrawable.md)
187
+ - [Integrations](docs/integrations.md) (Node scripts, React Native icons, Gradle, Vite/webpack, CI)
188
+ - [Comparison with `svg2vectordrawable` and Android Studio's Svg2Vector](docs/comparison.md)
101
189
 
102
190
  ## License
103
191
 
@@ -0,0 +1,162 @@
1
+ import { Config } from 'svgo';
2
+
3
+ /** Every warning code the converter can emit (stable, machine-readable). */
4
+ declare const WARNING_CODES: readonly ["unsupported-element", "unsupported-attribute", "unsupported-stroke-dasharray", "unsupported-stroke-gradient", "unsupported-paint", "unsupported-clip-path", "missing-gradient", "missing-clip-path", "gradient-under-skew", "gradient-bbox-unavailable", "group-skew", "opacity-approximated", "gradient-approximated", "unsupported-style", "empty-path", "min-sdk-exceeded", "long-path-data", "large-vector"];
5
+ /** Stable machine-readable warning codes. */
6
+ type WarningCode = (typeof WARNING_CODES)[number];
7
+ /**
8
+ * What a warning means for the output:
9
+ * - `lossy`: content is dropped or drawn wrong (an element, an attribute, a paint, a clip…);
10
+ * - `approximation`: everything is drawn, but the rendering differs slightly from the SVG;
11
+ * - `info`: the output is faithful; Android-side advice (API level, lint-style performance hints).
12
+ */
13
+ type WarningCategory = 'lossy' | 'approximation' | 'info';
14
+ /** The category of every warning code (see {@link WarningCategory}); drives the `strict` presets. */
15
+ declare const WARNING_CATEGORIES: Readonly<Record<WarningCode, WarningCategory>>;
16
+ /**
17
+ * `strict` presets: `true` turns every `lossy` and `approximation` warning into an error, `'lossy'`
18
+ * only the `lossy` ones. `info` codes are never escalated by `strict` (only by `rules`).
19
+ */
20
+ type StrictMode = boolean | 'lossy';
21
+ /** What to do with a warning: drop it, report it, or throw a `ConversionError`. */
22
+ type Severity = 'off' | 'warn' | 'error';
23
+ /** A non-fatal issue encountered during conversion. */
24
+ interface Warning {
25
+ /** Stable machine-readable code, e.g. `unsupported-element`. */
26
+ code: WarningCode;
27
+ /** Human-readable explanation. */
28
+ message: string;
29
+ /** SVG element/attribute the warning relates to, when known. */
30
+ node?: string;
31
+ }
32
+ interface ConvertOptions {
33
+ /**
34
+ * Run svgo normalization first (inline styles, shapes→paths, bake transforms…).
35
+ * Strongly recommended; it is what makes conversion robust across SVG sources.
36
+ * @default true
37
+ */
38
+ optimize?: boolean;
39
+ /** Override the svgo config used for normalization (only when `optimize` is true). */
40
+ svgoConfig?: Config;
41
+ /** Decimal places kept for generated numbers (coordinates, radii). @default 3 */
42
+ floatPrecision?: number;
43
+ /** Concrete color substituted for `currentColor`. @default '#000000' */
44
+ currentColor?: string;
45
+ /**
46
+ * SVG paints unfilled shapes black by default. When true, paths without an
47
+ * explicit fill get `android:fillColor="#FF000000"`. @default true
48
+ */
49
+ fillBlackForUnfilled?: boolean;
50
+ /**
51
+ * Throw a `ConversionError` on the first warning of a category instead of reporting it:
52
+ * `true` for `lossy` and `approximation` codes, `'lossy'` for `lossy` codes only (see
53
+ * {@link WARNING_CATEGORIES}). `info` codes keep their default severity under any preset.
54
+ * @default false
55
+ */
56
+ strict?: StrictMode;
57
+ /**
58
+ * Per-code severity, overriding `strict` and the defaults. E.g.
59
+ * `{ strict: true, rules: { 'opacity-approximated': 'warn' } }` rejects anything lossy but
60
+ * tolerates approximated opacity; `{ rules: { 'long-path-data': 'warn' } }` enables that hint.
61
+ */
62
+ rules?: Partial<Record<WarningCode, Severity>>;
63
+ /** Indentation width (spaces). @default 4 */
64
+ indent?: number;
65
+ /** Prepend an XML declaration (`<?xml version="1.0" encoding="utf-8"?>`). @default false */
66
+ xmlTag?: boolean;
67
+ /** Add `android:tint` to the `<vector>`. Android color literal (e.g. `#AARRGGBB`), passed verbatim. */
68
+ tint?: string;
69
+ /** Called for every warning as it happens (in addition to the returned list). */
70
+ onWarn?: (warning: Warning) => void;
71
+ /**
72
+ * The app's `minSdk`: when the output needs a higher API level (see `ConvertResult.minSdk`),
73
+ * a `min-sdk-exceeded` warning names the features responsible. Unset: no check.
74
+ */
75
+ minSdk?: number;
76
+ }
77
+ interface ConvertResult {
78
+ /** The generated Android VectorDrawable XML. */
79
+ xml: string;
80
+ /** All non-fatal issues encountered. */
81
+ warnings: Warning[];
82
+ /**
83
+ * Lowest Android API level that renders the output natively: 24 when it uses gradients
84
+ * (`<aapt:attr>` complex colors) or `android:fillType`, otherwise 21 (VectorDrawable itself).
85
+ */
86
+ minSdk: number;
87
+ }
88
+ /** An RGBA color expressed as Android `#AARRGGBB`. */
89
+ type AndroidColor = `#${string}`;
90
+
91
+ /** Thrown when a warning's severity resolves to `'error'` (via `strict` or `rules`). */
92
+ declare class ConversionError extends Error {
93
+ /** The warning that stopped the conversion. */
94
+ readonly warning: Warning;
95
+ constructor(warning: Warning);
96
+ }
97
+
98
+ /**
99
+ * VectorDrawable → SVG preview: re-serializes a VectorDrawable XML string into a standalone SVG string
100
+ * that renders the same picture, to preview a drawable in a browser, an image viewer or a PR review.
101
+ *
102
+ * Android semantics reproduced here (AndroidX `VectorDrawableCompat` / framework `VectorDrawable`):
103
+ * - `<vector>`: the viewport is stretched to width × height (no aspect preservation), `android:alpha`
104
+ * applies to the whole drawing. `android:tint` / `android:tintMode` are ignored unless
105
+ * {@link PreviewOptions.applyTint} is set (the default `src_in` mode recolors every pixel).
106
+ * - `<group>`: local matrix = T(translate + pivot) · R(rotation) · S(scale) · T(−pivot), i.e. Android
107
+ * applies scale → rotate → translate around the pivot.
108
+ * - `<clip-path>`: clips every *subsequent* sibling of its group (and their descendants); several
109
+ * clip-paths in a group, or nested groups, intersect. Clip paths use the non-zero rule.
110
+ * - `<path>`: no fill unless `fillColor`; no stroke unless `strokeColor` and `strokeWidth` > 0;
111
+ * `fillAlpha` / `strokeAlpha` multiply the color alpha (or the gradient); defaults butt / miter / 4.
112
+ * - `<aapt:attr name="android:fillColor|strokeColor"><gradient>`: linear / radial gradients in the
113
+ * path's coordinate space (= SVG `userSpaceOnUse`), `tileMode` clamp / repeated / mirror.
114
+ *
115
+ * Unsupported input throws an `Error` naming the construct: sweep gradients (no SVG equivalent),
116
+ * resource / theme references instead of color literals (`@color/…`, `?attr/…`), unknown elements,
117
+ * malformed XML, a missing or non-positive viewport. `trimPathStart` / `trimPathEnd` /
118
+ * `trimPathOffset` (never produced by the converter) are ignored.
119
+ *
120
+ * Deliberately self-contained (its own tiny XML parser, no import from the converter): the test
121
+ * harness uses it to validate the converter's output, so a converter bug cannot be masked by shared
122
+ * code. It has no Node-only dependency and is exported from the browser entry too.
123
+ */
124
+ /** Options of {@link vectorDrawableToSvg}. */
125
+ interface PreviewOptions {
126
+ /**
127
+ * Pixels per dp for the SVG `width` / `height` (default `1`, i.e. mdpi: `24dp` → `24`). Use e.g.
128
+ * `4` for an xxxhdpi-sized preview. The viewBox is the vector's viewport whatever the density.
129
+ */
130
+ density?: number;
131
+ /**
132
+ * Apply the vector's `android:tint` (color literal only) with its `android:tintMode` (default
133
+ * `src_in`; `src_over`, `src_atop`, `multiply`, `screen`, `add` supported) as an SVG filter.
134
+ * Default `false`: the tint is ignored and the drawing keeps its own colors.
135
+ */
136
+ applyTint?: boolean;
137
+ }
138
+ /**
139
+ * Converts a VectorDrawable XML string into an equivalent standalone SVG string (preview).
140
+ *
141
+ * The SVG `width` / `height` are the vector's `android:width` / `android:height` in dp times
142
+ * `options.density` (default 1), its `viewBox` the viewport, stretched with `preserveAspectRatio="none"`
143
+ * like Android. See the module documentation for the reproduced semantics and the unsupported input,
144
+ * which throws an `Error` (e.g. a sweep gradient or a `@color/…` reference).
145
+ *
146
+ * ```ts
147
+ * import { convert, vectorDrawableToSvg } from 'svg-vectordrawable';
148
+ * const preview = vectorDrawableToSvg(convert(svg).xml, { density: 4 });
149
+ * ```
150
+ */
151
+ declare function vectorDrawableToSvg(xml: string, options?: PreviewOptions): string;
152
+
153
+ /** Options of the lite build: no svgo normalization, hence no `svgoConfig` and `optimize` false. */
154
+ type LiteConvertOptions = Omit<ConvertOptions, 'optimize' | 'svgoConfig'> & {
155
+ /** Always false in the lite build; `true` throws (use `svg-vectordrawable/browser`). */
156
+ optimize?: false;
157
+ };
158
+
159
+ /** Same as the main `convert`, with `optimize` forced to `false`. */
160
+ declare function convert(svg: string, options?: LiteConvertOptions): ConvertResult;
161
+
162
+ export { type AndroidColor, ConversionError, type LiteConvertOptions as ConvertOptions, type ConvertResult, type LiteConvertOptions, type PreviewOptions, type Severity, type StrictMode, WARNING_CATEGORIES, WARNING_CODES, type Warning, type WarningCategory, type WarningCode, convert, vectorDrawableToSvg };