# Serialize_MTG.ps1 # © Wyrrrd 30.01.2026 # This script bulk overlays a box with numbers over input images to have serialized versions of them, just as WotC does it. # The concept includes handling of any input image size (assuming it is roughly a scaled 63mm x 88mm MTG card design). # As the box to be applied is taken from a source image, there needs to be two pixel spaces: reference space (1500px x 2100px) # and image space (defined by the input image). They will be transformed via an automatically calculated scale. This scale # is not x/y separate and only a best effort to not squash the box image, so it might break with input images too far removed # from the MTG ratio. ### Parameters param ( # Total count of serialization, so how many versions you want of a card [Parameter(Mandatory = $true)] [int] $TotalCount, # Font of the numbers in serial box # BEWARE: The Gotham font family is only free for personal use. If you want to use it commercially, you have to license it. [string] $FontPath = "gothambold.otf", # Offsets as tuples @(x,y) # supplied in final image pixels (not in reference space) [int[]] $Offset, # Apply color map using colormap.png [switch] $UseColorMap ) ### Paths & constants # Path to magick executable, or just "magick" if it's in the PATH. $magick = "magick" # Static directories and files $inputDir = "input" $outputDir = "output" $backgroundImg = "background.png" $colormapImg = "colormap.png" # Reference card resolution $refResolution = @{ Width=1500; Height=2100 } # Hardcoded background / color map reference size and offset # background.png and color map.png MUST match size # offset will be overridden by Offset parameter later in image space, if supplied $backgroundBoxRef = @{ X=128; Y=1032; Width=346; Height=107 } # Text boxes (in reference space, relative to background box) $currentBoxRef = @{ X=25; Y=24; Width=135; Height=63 } $totalBoxRef = @{ X=190; Y=24; Width=135; Height=63 } ### Functions function DisplayFileSize { param ( $bytecount ) switch -Regex ([math]::truncate([math]::log($bytecount,1024))) { '^0' {"$bytecount Bytes"} '^1' {"{0:n2} KB" -f ($bytecount / 1KB)} '^2' {"{0:n2} MB" -f ($bytecount / 1MB)} '^3' {"{0:n2} GB" -f ($bytecount / 1GB)} '^4' {"{0:n2} TB" -f ($bytecount / 1TB)} Default {"{0:n2} Bytes" -f ($bytecount / 1KB)} } } function ScaleBox { param ( $box ) @{ X = [int]($box.X * $scale) Y = [int]($box.Y * $scale) Width = [int]($box.Width * $scale) Height = [int]($box.Height * $scale) } } ######## # MAIN # ######## ### Prepare input/output directory if (-not (Test-Path $inputDir)) { New-Item -ItemType Directory -Path $inputDir | Out-Null } $inputFiles = Get-ChildItem $inputDir -File | Where-Object { $_.Extension -match '\.(png|jpg|jpeg)$' } # Wait for input files to be added while ($inputFiles.Count -eq 0) { Write-Host "Input directory is empty." Write-Host "Please place input images into '$inputDir' directory, then continue." start $inputDir Pause $inputFiles = Get-ChildItem $inputDir -File | Where-Object { $_.Extension -match '\.(png|jpg|jpeg)$' } } if (-not (Test-Path $outputDir)) { New-Item -ItemType Directory -Force -Path $outputDir | Out-Null } ### Loop over input images foreach ($inputFile in $inputFiles) { $baseName = [IO.Path]::GetFileNameWithoutExtension($inputFile.Name) $ext = $inputFile.Extension $outputSubDir = Join-Path $outputDir $baseName if (-not (Test-Path $outputSubDir)) { New-Item -ItemType Directory -Force -Path $outputSubDir | Out-Null } # Input image size & scaling $imageInfo = & $magick identify -format "%w %h" $inputFile.FullName $image = @{} $image.Width, $image.Height = $imageInfo -split " " | ForEach-Object { [int]$_ } $scaleX = $image.Width / $refResolution.Width $scaleY = $image.Height / $refResolution.Height $scale = [Math]::Min($scaleX, $scaleY) ### Transform box positions and dimensions $backgroundBox = ScaleBox $backgroundBoxRef # Override with offset, if supplied if (-not $useDefaultOffset -and $Offset) { if ($Offset.Count -eq 2) { $backgroundBox.X = $Offset[0] $backgroundBox.Y = $Offset[1] } else { "`nThe given offset was malformed, please enter it as '@(x,y)'.`nDo you want to continue with the default offset?" | Write-Warning -WarningAction Inquire $useDefaultOffset = true } } $currentBox = ScaleBox $currentBoxRef $totalBox = ScaleBox $totalBoxRef ### Sanity checks # Total file size >500MB check if ($inputFile.Length * $totalCount -gt 524288000) { $totalFileSize = DisplayFileSize($inputFile.Length * $totalCount) "`nYou specified a total count of $totalCount for $inputFile.`nThis would result in new image files using up approximately $totalFileSize of your disk." | Write-Warning -WarningAction Inquire } Write-Host "Starting to serialize $inputFile." # Out of bounds check if ( $backgroundBox.X + $backgroundBox.Width -gt $image.Width -or $backgroundBox.Y + $backgroundBox.Height -gt $image.Height) { if (-not $useDefaultOffset -and $Offset) { Write-Host -NoNewline "The given " } else { Write-Host -NoNewline "The default " } Write-Host "offset would cause the serial box to be (partially) outside of the input image's boundaries.`nSkipping input image $inputFile." continue } else { Write-Host -NoNewline "Using " if (-not $useDefaultOffset -and $Offset) { Write-Host -NoNewline "given " } else { Write-Host -NoNewline "scaled default " } Write-Host -NoNewline "offset of '@($($backgroundBox.X),$($backgroundBox.Y))'" if ($UseColorMap) { Write-Host -NoNewline " with color map" } Write-Host "." } ### Temporary files $currentFile = Join-Path $outputSubDir "_current.png" $totalFile = Join-Path $outputSubDir "_total.png" $boxFile = Join-Path $outputSubDir "_box.png" ### Generate total number file $totalStr = $TotalCount.ToString() & $magick ` -size "$($totalBox.Width)x$($totalBox.Height)" ` -background none ` -fill white ` -font $FontPath ` -kerning 3 ` -gravity center ` caption:"$totalStr" ` $totalFile ### Serialization padding $padLength = $totalStr.Length ### Per-card generation of current number for ($i = 1; $i -le $TotalCount; $i++) { ### Generate current number file $currentStr = $i.ToString("D$padLength") & $magick ` -size "$($currentBox.Width)x$($currentBox.Height)" ` -background none ` -fill white ` -font $FontPath ` -kerning 3 ` -gravity center ` caption:"$currentStr" ` $currentFile ### Output file $outputFile = Join-Path $outputSubDir ("{0}_{1}{2}" -f $baseName, $currentStr, $ext) ### Compose file # Create box with numbers & $magick ` "(" $backgroundImg -resize "$($backgroundBox.Width)x$($backgroundBox.Height)!" ")" ` $currentFile -geometry +$($currentBox.X)+$($currentBox.Y) -composite ` $totalFile -geometry +$($totalBox.X)+$($totalBox.Y) -composite ` $boxFile if ($UseColorMap) { # Convert to mask & $magick ` $boxFile -background black -alpha remove -alpha off ` $boxFile # Mask color map & $magick ` "(" $colormapImg -resize "$($backgroundBox.Width)x$($backgroundBox.Height)!" ")" ` $boxFile -compose CopyOpacity -composite ` $boxFile # Re-add background & $magick ` "(" $backgroundImg -resize "$($backgroundBox.Width)x$($backgroundBox.Height)!" ")" ` $boxFile -colorspace srgb -composite ` $boxFile } # Compose over input image at offset location & $magick ` $inputFile.FullName ` $boxFile -geometry +$($backgroundBox.X)+$($backgroundBox.Y) -composite ` $outputFile Write-Host -NoNewline "`r($i/$totalCount)" } ### Cleanup Remove-Item $currentFile Remove-Item $totalFile Remove-Item $boxFile Write-Host "`n" } Write-Host "All cards serialized successfully." start $outputDir