Search by

sportsgearswag / artwork-vectorizer

dishank.kumar

Converts raster artwork into colour-separated, print-ready SVG: one editable layer per ink, snapped to a PMS palette.

Package info

github.com/SportsGearSwag/artwork-vectorizer

pkg:composer/sportsgearswag/artwork-vectorizer

Statistics

Installs: 19

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.1 2026-09-07 07:35 UTC

This package is not auto-updated.

Last update: 2026-09-08 05:44:09 UTC


README

Turns raster artwork into a colour-separated, print-ready SVG: one editable <g> per ink, plain hex fills, every colour snapped to its nearest Pantone Coated match. Built for screen printing, where the number of separations is the thing that costs money.

input:  a 900x400 PNG logo, 403 unique colours after anti-aliasing
output: 4 layers -> WHITE, PMS 186 C, PMS 282 C, PMS 1235 C
        15 subpaths, 3.9 KB, 1.4s, 1.6% off the original

Quick start

Two commands and one line, in any Symfony project:

composer require sportsgearswag/artwork-vectorizer
apt-get install -y imagemagick potrace      # plus VTracer — see Requirements
// config/bundles.php
Sgs\Vectorizer\ArtworkVectorizerBundle::class => ['all' => true],

Then inject it and go — no service wiring, no parameters, no configuration:

public function __construct(private readonly VectorizerService $vectorizer) {}

$result = $this->vectorizer->convert($file, TraceOptions::fromPreset('logo'));
// $result['svg'], $result['layers'], ...

Requirements

All three are required for production. The package technically runs on ImageMagick alone, but only by falling back to a pure-PHP tracer that emits straight segments with no curve fitting — correct, and wrong for a logo.

PHP 8.2+
Symfony 6.4, 7.x or 8.x (any, or none — the library works standalone)
ImageMagick colour separation. 7 (magick) or 6.x (convert), both supported
potrace highest edge accuracy — logos, text, hard edges
VTracer fewer path segments, faster — busy and shaded artwork

Install both tracers, not one. They are not interchangeable, and the auto preset picks between them per artwork; with only one installed every job goes through it whether it suits or not. On the same flat four-ink logo:

engine subpaths size time difference from original
potrace 15 3.8 KB 0.75s 1.61%
VTracer 14 2.6 KB 0.65s 2.69%

potrace wins on fidelity, VTracer on file size and speed — and the gap widens the other way on busy artwork, where potrace emits far more segments.

ImageMagick and potrace

apt-get update && apt-get install -y imagemagick potrace   # debian / ubuntu
apk add --no-cache imagemagick potrace                     # alpine
brew install imagemagick potrace                           # macos

VTracer

VTracer is a Rust binary and is not in apt, apk or brew — install the static release binary and put it on PATH:

# linux x86_64
curl -sL https://github.com/visioncortex/vtracer/releases/latest/download/vtracer-x86_64-unknown-linux-musl.tar.gz \
  | tar xz -C /usr/local/bin vtracer

# linux arm64
curl -sL https://github.com/visioncortex/vtracer/releases/latest/download/vtracer-aarch64-unknown-linux-musl.tar.gz \
  | tar xz -C /usr/local/bin vtracer

# macos apple silicon  (use x86_64-apple-darwin on intel)
curl -sL https://github.com/visioncortex/vtracer/releases/latest/download/vtracer-aarch64-apple-darwin.tar.gz \
  | tar xz -C /usr/local/bin vtracer

The musl builds are static, so they need no runtime libraries and work on Alpine as well as glibc distros. cargo install vtracer also works if Rust is already available.

Both binaries must be on PATH on every machine that runs a conversion — each developer's machine, CI, and every deployed image. This is the step teams miss: it works locally and returns No tracing engine is installed on this server in production.

Check what a given server actually has:

foreach ($vectorizer->engines() as $name => $engine) {
    printf("%-8s %s\n", $name, $engine['available'] ? 'ready' : 'not installed');
}

Install

Published on Packagist, so there is no repositories block and no token to configure:

composer require sportsgearswag/artwork-vectorizer

Use ^1.1 or newer if you pin a constraint. The bundle does not exist in 1.0, so a fresh install of 1.0.0 fails at container compile with no obvious cause. Symfony 8 needs ^1.1.1; earlier versions cap Symfony at 7.

Symfony

Register the bundle (Flex does this for you):

// config/bundles.php
return [
    // ...
    Sgs\Vectorizer\ArtworkVectorizerBundle::class => ['all' => true],
];

That is the whole integration — every service is wired for you. Inject VectorizerService anywhere:

use Sgs\Vectorizer\TraceOptions;
use Sgs\Vectorizer\VectorizerService;

public function __construct(private readonly VectorizerService $vectorizer) {}

public function convert(string $uploadedFile): array
{
    $detection = $this->vectorizer->inspect($uploadedFile);
    $options   = TraceOptions::fromPreset($detection['preset']);

    return $this->vectorizer->convert($uploadedFile, $options);
}

Configuration is optional — these are the defaults:

# config/packages/artwork_vectorizer.yaml
artwork_vectorizer:
    tmp_dir: '%kernel.project_dir%/var/tmp'   # wiped after every conversion
    palette: json                             # bundled Pantone Coated deck
    palette_file: ~                           # or point at your own json
    binaries:
        magick: magick                        # absolute paths are fine
        potrace: potrace
        vtracer: vtracer
    timeouts:
        magick: 120
        trace: 180                            # raise for very large artwork

To feed inks from your own source — a database table, an API — implement PaletteProviderInterface and name your service:

artwork_vectorizer:
    palette: App\Vectorizer\DoctrinePmsProvider

Without Symfony

Construct it by hand; see examples/convert.php.

Handing this to another team

It is on Packagist and the repository is public, so there is nothing to grant and nothing to configure. Send them these three steps.

1. Require it:

composer require sportsgearswag/artwork-vectorizer

2. Register the bundle — one line, and then every service is wired:

// config/bundles.php
Sgs\Vectorizer\ArtworkVectorizerBundle::class => ['all' => true],

3. Install the binaries on every machine that runs it — their local machines, CI, and each deployed image. This is the step teams forget, and the failure mode is a conversion that "works locally" and returns No tracing engine is installed on this server in production. See Requirements; the short version is ImageMagick is mandatory and both tracers are required — note VTracer is not in apt/apk/brew and needs its release binary.

4. Confirm the licence with the copyright holder. This package is proprietary (see LICENSE) — being able to clone a public repository is not the same as being licensed to use it. The owner saying yes is all that is needed, but get it in writing.

What they do not need

  • Any access token, deploy key or auth.json — the repository is public
  • A repositories block in composer.json — it resolves from Packagist
  • A Packagist account
  • Any service configuration — the defaults work; see Symfony
  • To copy any service definitions. If they find themselves writing Sgs\Vectorizer\... entries in services.yaml, they have missed step 2.

Worked example

sgs-designer is the reference integration. In full it is:

File Change
composer.json one require line
config/bundles.php one line
src/Controller/.../ArtworkVectorizerController.php inject VectorizerService, one upload route, one convert route returning JSON
templates/.../index.html.twig the upload form and the result view

No service wiring, no parameters, no compiler passes.

What convert() returns

The whole contract for building a UI on top of it:

[
    'svg'              => '<svg ...>',   // the finished markup
    'layers'           => [              // one entry per ink, largest area first
        ['hex' => '#C8102E', 'share' => 12.7, 'pms' => 'PMS 186 C', 'pmsDelta' => 0.0, 'subpaths' => 2],
    ],
    'palette'          => [['hex' => '#C8102E', 'share' => 12.7]],
    'detectedInks'     => 4,      // found before the max-colour cap was applied
    'paletteTruncated' => false,  // true when detectedInks exceeded the cap
    'width'            => 900,
    'height'           => 400,
    'bytes'            => 3930,
    'subpaths'         => 15,
    'engine'           => 'potrace',  // which one actually ran, after fallback
    'compliance'       => [],         // non-empty means the SVG broke a format rule
    'seconds'          => 1.55,
]

Two of those are easy to miss and worth surfacing in any UI. engine is what actually ran — ask for vtracer on a server without it and this comes back potrace, silently. And paletteTruncated tells you the artwork had more inks than the preset allowed, which is the honest reason a result looks flat.

Presets

Pick by artwork type. inspect() will choose for you.

Preset Max inks For
logo 6 Spot-colour logos. Fewest layers, exact brand colours.
detailed 10 Keeps small accents and thin outlines.
illustration 24 Mascots, sticker and tee art.
photo 24 Posterised likeness. Not print-ready.
max_detail 48 Complex artwork. Slow, and the SVG gets large.

Output

<svg xmlns="http://www.w3.org/2000/svg" width="900" height="400" viewBox="0 0 900 400" fill="none">
  <g id="color-1" data-color="#C8102E" data-area="12.7" data-pms="PMS 186 C" data-pms-delta="0" fill="#C8102E">
    <path d="..."/>
  </g>
</svg>

Paths with plain hex fills only — no gradients, filters, clip paths or embedded raster — so it loads into any design editor. SvgAssembler::complianceIssues() asserts that.

Measuring quality

convert() is cheap. The two checks below re-render the SVG and compare it to the source, which costs 20-30 seconds, so call them only when you want the number:

$deviation = $vectorizer->measureDeviation($file, $result['svg'], true);   // % off the original
$coverage  = $vectorizer->measureCoverage($file, $result['svg']);          // % of ink area drawn

$verdict = ConversionAssessment::assess($deviation, $coverage, $preset, count($result['layers']));
// ['verdict' => 'good', 'headline' => 'Good — 1.61% off the original across 4 layers', 'detail' => '...']

A flat logo should land under ~3%. Gradients and photographs will not: they have no exact answer in flat inks, and assess() returns escalate rather than pretending otherwise.

Tests

composer install
vendor/bin/phpunit

The suite converts a real four-ink fixture and asserts the ink count, the Pantone matches, SVG compliance and a deviation ceiling. It skips if ImageMagick is missing, so CI runs --fail-on-skipped.

Licence

Proprietary — see LICENSE, which also carries the Pantone trademark notice and the licences of the external tools this package invokes.