Convert-SVG.ps1

<#PSScriptInfo
.VERSION 1.2.0
.GUID b51d2447-b40d-47de-8ca5-8583db061ba4
.AUTHOR Giovanni Solone
.TAGS powershell inkscape images conversion svg png jpg
.LICENSEURI https://opensource.org/licenses/MIT
.PROJECTURI https://github.com/gioxx/Nebula.Scripts/blob/main/Utility/Convert-SVG.ps1
#>


#Requires -Version 7.0

<#
.SYNOPSIS
Converts SVG files to PNG or JPG format, or PNG/JPG files to SVG format, using Inkscape or built-in .NET APIs.
 
.DESCRIPTION
This script converts SVG files to PNG or JPG format using Inkscape.
PNG format preserves transparency, while JPG format does not support transparency
but may result in smaller file sizes with white background.
 
It also supports the reverse conversion: PNG or JPG to SVG.
For PNG-to-SVG conversion, the PNG is embedded as base64 inside the SVG container,
preserving the original image pixel-perfectly (including transparency for PNG files).
This conversion uses built-in .NET APIs (no external dependencies required),
with Inkscape as fallback.
 
Requires Inkscape to be installed on the system (for SVG-to-raster export).
Download from: https://inkscape.org/release/
 
.PARAMETER InputFile
The path to the input file to be converted. Must be a valid SVG, PNG, or JPG file.
 
.PARAMETER OutputFormat
The output format for conversion. Valid values are 'PNG', 'JPG', and 'SVG'. Default is 'PNG'.
When InputFile is an SVG, valid targets are PNG and JPG.
When InputFile is a PNG or JPG, the only valid target is SVG.
 
.PARAMETER InkscapePath
The path to the Inkscape executable. If omitted, the script searches common install paths and PATH.
 
.PARAMETER Width
The width of the output image in pixels (only used when converting SVG to PNG/JPG). Default is 240.
 
.PARAMETER Height
The height of the output image in pixels (only used when converting SVG to PNG/JPG). Default is 240.
 
.PARAMETER OutputFile
Optional custom output file path. If not specified, uses input filename with new extension.
 
.PARAMETER PreferInkscapeRasterToSvg
When converting PNG/JPG to SVG, try Inkscape first instead of the built-in .NET wrapper approach.
This is useful if you want to keep the whole conversion flow inside Inkscape.
 
.EXAMPLE
.\Convert-SVG.ps1 -InputFile "C:\path\to\image.svg"
Converts the SVG file to PNG using default settings.
 
.EXAMPLE
.\Convert-SVG.ps1 -InputFile "C:\path\to\image.svg" -OutputFormat JPG
Converts the SVG file to JPG format (loses transparency).
 
.EXAMPLE
.\Convert-SVG.ps1 -InputFile "C:\path\to\image.svg" -Width 512 -Height 512
Converts SVG to PNG with custom dimensions (512x512 pixels).
 
.EXAMPLE
.\Convert-SVG.ps1 -InputFile "C:\path\to\image.png" -OutputFormat SVG
Converts a PNG to SVG with the image embedded as base64 (transparency preserved).
 
.EXAMPLE
.\Convert-SVG.ps1 -InputFile "image.svg" -OutputFile "custom-output.png" -InkscapePath "D:\Tools\inkscape.exe"
Converts using custom output path and Inkscape location.
 
.EXAMPLE
.\Convert-SVG.ps1 -InputFile "image.png" -OutputFormat SVG -PreferInkscapeRasterToSvg
Uses Inkscape first for the PNG-to-SVG conversion instead of the embedded-base64 .NET path.
 
.NOTES
Ensure Inkscape is installed on your system for SVG-to-raster conversions.
Download Inkscape from: https://inkscape.org/release/
 
For JPG conversion, transparency is replaced with white background.
PNG format is recommended when transparency preservation is needed.
 
For PNG/JPG-to-SVG conversion, the raster image is embedded as base64 inside the SVG.
This is not a vector tracing — the image remains pixel-based but is scalable as an SVG container.
No external dependencies are required: conversion uses built-in .NET System.Drawing APIs.
 
Known Issue: Inkscape may return exit code 1 even on successful conversions.
This script checks for actual file creation to determine success.
 
Modification History:
v1.2.0 (2026-06-11): Added PNG/JPG to SVG conversion support and a dynamic Inkscape path resolver, with optional Inkscape-first raster handling.
v1.0.1 (2026-03-26): Fixed PROJECTURI in the script metadata to point to the correct GitHub repository and file.
v1.0.0 (2024-10-02): Initial release.
#>


[CmdletBinding()]
param (
    [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
    [ValidateScript({
            if (-not (Test-Path $_ -PathType Leaf)) {
                throw "File not found: $_"
            }
            $ext = [System.IO.Path]::GetExtension($_).ToLower()
            if ($ext -notin @('.svg', '.png', '.jpg', '.jpeg')) {
                throw "File must be SVG, PNG, or JPG: $_"
            }
            return $true
        })]
    [string]$InputFile,
    [Parameter()]
    [ValidateSet('PNG', 'JPG', 'SVG')]
    [string]$OutputFormat = 'PNG',
    [Parameter()]
    [string]$InkscapePath,
    [Parameter()]
    [ValidateRange(1, 8192)]
    [int]$Width = 240,
    [Parameter()]
    [ValidateRange(1, 8192)]
    [int]$Height = 240,
    [Parameter()]
    [string]$OutputFile
    ,
    [Parameter()]
    [switch]$PreferInkscapeRasterToSvg
)

# ---------------------------------------------------------------------------
# Helper: convert raster (PNG/JPG) to SVG by embedding as base64
# Uses built-in .NET APIs only — no Python or external tools required.
# ---------------------------------------------------------------------------
function Convert-RasterToSvg {
    param(
        [string]$InputPath,
        [string]$Output,
        [switch]$PreferInkscapeRasterToSvg,
        [string]$ResolvedInkscapePath
    )

    try {
        if ($PreferInkscapeRasterToSvg -and $ResolvedInkscapePath) {
            Write-Verbose "Trying Inkscape first for raster-to-SVG..."
            $inkscapeArgs = @(
                $InputPath
                "--export-type=svg"
                "--export-filename=$Output"
            )
            & "$ResolvedInkscapePath" $inkscapeArgs 2>$null
            Start-Sleep -Milliseconds 1500
            if (Test-Path -LiteralPath $Output) {
                Write-Verbose "Inkscape conversion succeeded."
                return $true
            }
            Write-Verbose "Inkscape did not create the output; falling back to .NET wrapper."
        }

        # Read image dimensions via System.Drawing
        Add-Type -AssemblyName System.Drawing
        $img = [System.Drawing.Image]::FromFile($InputPath)
        $w = $img.Width
        $h = $img.Height
        $img.Dispose()

        # Detect MIME type from extension
        $ext = [System.IO.Path]::GetExtension($InputPath).ToLower()
        $mime = if ($ext -in @('.jpg', '.jpeg')) { 'image/jpeg' } else { 'image/png' }

        # Encode file as base64
        $bytes = [System.IO.File]::ReadAllBytes($InputPath)
        $base64 = [System.Convert]::ToBase64String($bytes)

        # Build SVG
        $svg = @"
<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink"
     width="$w" height="$h"
     viewBox="0 0 $w $h">
  <image width="$w" height="$h"
         href="data:$mime;base64,$base64"/>
</svg>
"@

        [System.IO.File]::WriteAllText($Output, $svg, [System.Text.Encoding]::UTF8)
        Write-Verbose ".NET conversion succeeded: ${w}x${h}"
        return $true

    }
    catch {
        Write-Warning ".NET conversion failed: $($_.Exception.Message)"

        # Fallback: Inkscape
        if ($ResolvedInkscapePath) {
            Write-Verbose "Trying Inkscape as fallback..."
            $inkscapeArgs = @(
                $InputPath
                "--export-type=svg"
                "--export-filename=$Output"
            )
            & "$ResolvedInkscapePath" $inkscapeArgs 2>$null
            Start-Sleep -Milliseconds 1500
            return (Test-Path $Output)
        }

        Write-Error "Conversion failed and Inkscape fallback is not available."
        return $false
    }
}

function Resolve-InkscapePath {
    param(
        [string]$ExplicitPath
    )

    $candidatePaths = @()

    if ($ExplicitPath) {
        $candidatePaths += $ExplicitPath
    }

    if ($env:ProgramFiles) {
        $candidatePaths += (Join-Path -Path $env:ProgramFiles -ChildPath 'Inkscape\bin\inkscape.exe')
    }

    if (${env:ProgramFiles(x86)}) {
        $candidatePaths += (Join-Path -Path ${env:ProgramFiles(x86)} -ChildPath 'Inkscape\bin\inkscape.exe')
    }

    foreach ($path in $candidatePaths) {
        if ($path -and (Test-Path -LiteralPath $path)) {
            return $path
        }
    }

    $cmd = Get-Command inkscape.exe -ErrorAction SilentlyContinue |
        Select-Object -ExpandProperty Source -First 1
    if (-not $cmd) {
        $cmd = Get-Command inkscape -ErrorAction SilentlyContinue |
            Select-Object -ExpandProperty Source -First 1
    }

    if ($cmd) {
        return $cmd
    }

    return $null
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
try {
    $InputFile = (Resolve-Path -LiteralPath $InputFile -ErrorAction Stop).Path
    $ResolvedInkscapePath = Resolve-InkscapePath -ExplicitPath $InkscapePath
    $inputExt = [System.IO.Path]::GetExtension($InputFile).ToLower()
    $isRaster = $inputExt -in @('.png', '.jpg', '.jpeg')

    # Validate direction
    if ($isRaster -and $OutputFormat -ne 'SVG') {
        throw "Raster input ($inputExt) can only be converted to SVG. Use -OutputFormat SVG."
    }
    if (-not $isRaster -and $OutputFormat -eq 'SVG') {
        throw "SVG-to-SVG conversion is not supported. Use PNG or JPG as output format."
    }

    # Determine output path
    if (-not $OutputFile) {
        $extension = switch ($OutputFormat) {
            'JPG' { '.jpg' }
            'SVG' { '.svg' }
            default { '.png' }
        }
        $OutputFile = [System.IO.Path]::ChangeExtension($InputFile, $extension)
    }

    Write-Verbose "Input: $InputFile"
    Write-Verbose "Output: $OutputFile"

    if (Test-Path $OutputFile) {
        Remove-Item $OutputFile -Force
        Write-Verbose "Removed existing output file"
    }

    # ---------------------------------------------------------------------------
    # Raster -> SVG
    # ---------------------------------------------------------------------------
    if ($isRaster) {
        Write-Host "Converting raster image to SVG (embedded base64)..." -ForegroundColor Cyan
        $ok = Convert-RasterToSvg -InputPath $InputFile -Output $OutputFile -PreferInkscapeRasterToSvg:$PreferInkscapeRasterToSvg -ResolvedInkscapePath $ResolvedInkscapePath
        if (-not $ok -or -not (Test-Path $OutputFile)) {
            throw "Conversion failed: output file not created: $OutputFile"
        }
    }
    # ---------------------------------------------------------------------------
    # SVG -> PNG / JPG (original Inkscape path)
    # ---------------------------------------------------------------------------
    else {
        if (-not $ResolvedInkscapePath) {
            if ($InkscapePath) {
                Write-Error "Inkscape not found at: $InkscapePath"
            } else {
                Write-Error "Inkscape not found in common install paths or PATH."
            }
            Write-Host "Please install Inkscape from: https://inkscape.org/release/" -ForegroundColor Yellow
            exit 1
        }

        $inkscapeArgs = @(
            "--export-type=$($OutputFormat.ToLower())"
            "--export-filename=$OutputFile"
            "-w", $Width
            "-h", $Height
        )

        if ($OutputFormat -eq 'JPG') {
            $inkscapeArgs += '--export-background=white'
            $inkscapeArgs += '--export-background-opacity=1'
            Write-Verbose "JPG format: adding white background to remove transparency"
        }

        $inkscapeArgs += $InputFile
        Write-Verbose "Executing: `"$ResolvedInkscapePath`" $($inkscapeArgs -join ' ')"
        & "$ResolvedInkscapePath" $inkscapeArgs 2>$null

        # Inkscape often returns exit code 1 even on success
        Start-Sleep -Milliseconds 1500
    }

    # ---------------------------------------------------------------------------
    # Result
    # ---------------------------------------------------------------------------
    if (Test-Path $OutputFile) {
        $outputInfo = Get-Item $OutputFile
        Write-Host "✅ Successfully converted to $OutputFormat`: $($outputInfo.FullName)" -ForegroundColor Green
        Write-Host " File size: $([math]::Round($outputInfo.Length / 1KB, 2)) KB" -ForegroundColor Gray
        return $outputInfo.FullName
    }
    else {
        throw "Conversion failed: output file not created: $OutputFile"
    }

}
catch {
    Write-Error "❌ Error during conversion: $($_.Exception.Message)"
    exit 1
}