param( [switch]$Force ) $ErrorActionPreference = "Stop" Add-Type -AssemblyName System.Drawing $Root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path $AssetRoot = if ($env:ASSET_ROOT) { $env:ASSET_ROOT } else { (Resolve-Path (Join-Path $Root "..\..\jv-assets")).Path } $OutRoot = Join-Path $Root "content_population_exports" $ProductsRoot = Join-Path $Root "data\content\products" $IntelRoot = Join-Path $Root "data\intelligence" $GeneratedAt = (Get-Date).ToUniversalTime().ToString("o") New-Item -ItemType Directory -Force -Path $AssetRoot | Out-Null function Read-JsonFile($Path, $Fallback = $null) { if (-not (Test-Path -LiteralPath $Path)) { return $Fallback } return (Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json) } function ConvertTo-PlainText($Value) { if ($null -eq $Value) { return "" } if ($Value -is [array]) { return (($Value | ForEach-Object { ConvertTo-PlainText $_ }) -join " ยท ") } $text = [string]$Value $text = $text -replace "[\u2013\u2014]", "-" $text = $text -replace "[^\u0009\u000A\u000D\u0020-\u007E]", "" $text = $text -replace "\s+", " " return $text.Trim() } function Wrap-Text([System.Drawing.Graphics]$Graphics, [string]$Text, [System.Drawing.Font]$Font, [int]$MaxWidth, [int]$MaxLines = 4) { $words = (ConvertTo-PlainText $Text) -split "\s+" $lines = New-Object System.Collections.Generic.List[string] $line = "" foreach ($word in $words) { $candidate = if ($line) { "$line $word" } else { $word } if ($Graphics.MeasureString($candidate, $Font).Width -le $MaxWidth) { $line = $candidate } else { if ($line) { $lines.Add($line) } $line = $word if ($lines.Count -ge $MaxLines) { break } } } if ($line -and $lines.Count -lt $MaxLines) { $lines.Add($line) } if ($lines.Count -eq $MaxLines -and $words.Count -gt 0) { $last = $lines[$lines.Count - 1] if ($Graphics.MeasureString("$last ...", $Font).Width -le $MaxWidth) { $lines[$lines.Count - 1] = "$last ..." } } return $lines.ToArray() } function Get-ProductThemes($Product, $Sku) { $themes = New-Object System.Collections.Generic.List[string] foreach ($benefit in @($Product.metafields.pdpProductBenefits)) { $t = ConvertTo-PlainText $benefit if ($t) { $themes.Add($t) } } $aspects = Read-JsonFile (Join-Path $IntelRoot "$Sku\review-aspects.json") foreach ($aspect in @($aspects.aspects | Select-Object -First 3)) { $t = ConvertTo-PlainText $aspect.aspect if ($t -and -not $themes.Contains($t)) { $themes.Add($t) } } if ($themes.Count -eq 0) { $themes.Add("Customer-language extraction pending") $themes.Add("Evidence review required before approval") $themes.Add("Draft visual asset for staging QA") } return @($themes | Select-Object -First 4) } function Draw-DraftAsset($TargetPath, $Product, [string]$Field) { $isMobile = $Field -eq "pdpResultsMobile" $isCompare = $Field -eq "pdpHowWeCompare" $width = if ($isMobile) { 1080 } else { 1600 } $height = if ($isMobile) { 1350 } else { 1000 } $bitmap = New-Object System.Drawing.Bitmap($width, $height) $g = [System.Drawing.Graphics]::FromImage($bitmap) $g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias $g.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAliasGridFit $bg = [System.Drawing.Color]::FromArgb(247, 248, 255) $brand = [System.Drawing.Color]::FromArgb(50, 50, 125) $accent = [System.Drawing.Color]::FromArgb(245, 158, 11) $muted = [System.Drawing.Color]::FromArgb(91, 97, 120) $border = [System.Drawing.Color]::FromArgb(220, 225, 245) $white = [System.Drawing.Color]::White $g.Clear($bg) $brandBrush = New-Object System.Drawing.SolidBrush($brand) $accentBrush = New-Object System.Drawing.SolidBrush($accent) $mutedBrush = New-Object System.Drawing.SolidBrush($muted) $whiteBrush = New-Object System.Drawing.SolidBrush($white) $borderPen = New-Object System.Drawing.Pen($border, 4) $brandPen = New-Object System.Drawing.Pen($brand, 8) $titleFont = New-Object System.Drawing.Font("Arial", $(if ($isMobile) { 52 } else { 58 }), [System.Drawing.FontStyle]::Bold) $subFont = New-Object System.Drawing.Font("Arial", $(if ($isMobile) { 30 } else { 32 }), [System.Drawing.FontStyle]::Regular) $bodyFont = New-Object System.Drawing.Font("Arial", $(if ($isMobile) { 28 } else { 30 }), [System.Drawing.FontStyle]::Regular) $smallFont = New-Object System.Drawing.Font("Arial", $(if ($isMobile) { 22 } else { 22 }), [System.Drawing.FontStyle]::Bold) $draftFont = New-Object System.Drawing.Font("Arial", $(if ($isMobile) { 24 } else { 24 }), [System.Drawing.FontStyle]::Bold) $margin = if ($isMobile) { 72 } else { 96 } $contentWidth = $width - (2 * $margin) $g.FillRectangle($brandBrush, 0, 0, $width, $(if ($isMobile) { 118 } else { 110 })) $g.DrawString("JUST VITAMINS", $smallFont, $whiteBrush, $margin, 36) $g.DrawString("DRAFT ASSET - NEEDS HUMAN IMAGE APPROVAL", $draftFont, $accentBrush, $width - $margin - 520, 36) $y = if ($isMobile) { 170 } else { 160 } $assetLabel = if ($Field -eq "pdpResults") { "Results / benefit visual" } elseif ($Field -eq "pdpResultsMobile") { "Mobile results visual" } else { "How we compare visual" } $g.DrawString($assetLabel.ToUpperInvariant(), $smallFont, $accentBrush, $margin, $y) $y += 48 foreach ($line in (Wrap-Text $g (ConvertTo-PlainText $Product.name) $titleFont $contentWidth 2)) { $g.DrawString($line, $titleFont, $brandBrush, $margin, $y) $y += [int]($titleFont.GetHeight($g) + 6) } $y += 24 $boxHeight = if ($isMobile) { 560 } else { 410 } $rect = New-Object System.Drawing.Rectangle($margin, $y, $contentWidth, $boxHeight) $g.FillRectangle($whiteBrush, $rect) $g.DrawRectangle($borderPen, $rect) $innerX = $margin + 44 $innerY = $y + 42 $headline = if ($isCompare) { "Comparison content is queued for review" } else { "Customer-led message scaffold" } $g.DrawString($headline, $subFont, $brandBrush, $innerX, $innerY) $innerY += 72 $themes = Get-ProductThemes $Product $Product.sku $idx = 1 foreach ($theme in $themes) { $g.FillEllipse($accentBrush, $innerX, $innerY + 8, 30, 30) $g.DrawString([string]$idx, $smallFont, $whiteBrush, $innerX + 8, $innerY + 6) foreach ($line in (Wrap-Text $g $theme $bodyFont ($contentWidth - 130) 2)) { $g.DrawString($line, $bodyFont, $mutedBrush, $innerX + 54, $innerY) $innerY += [int]($bodyFont.GetHeight($g) + 3) } $innerY += 20 $idx += 1 } $footerY = $height - 120 $g.DrawLine($borderPen, $margin, $footerY - 26, $width - $margin, $footerY - 26) $footer = "Generated from dashboard evidence only. Not approved. Replace with final creative after quote-level, compliance, and image QA." foreach ($line in (Wrap-Text $g $footer $smallFont $contentWidth 2)) { $g.DrawString($line, $smallFont, $mutedBrush, $margin, $footerY) $footerY += [int]($smallFont.GetHeight($g) + 4) } $bitmap.Save($TargetPath, [System.Drawing.Imaging.ImageFormat]::Png) $g.Dispose() $bitmap.Dispose() foreach ($obj in @($brandBrush, $accentBrush, $mutedBrush, $whiteBrush, $borderPen, $brandPen, $titleFont, $subFont, $bodyFont, $smallFont, $draftFont)) { $obj.Dispose() } } $rows = New-Object System.Collections.Generic.List[object] $products = Get-ChildItem -LiteralPath $ProductsRoot -Filter "*.json" | ForEach-Object { Read-JsonFile $_.FullName } foreach ($product in $products) { foreach ($field in @("pdpResults", "pdpResultsMobile", "pdpHowWeCompare")) { $filename = ConvertTo-PlainText $product.metafields.$field if (-not $filename) { continue } $target = Join-Path $AssetRoot $filename $exists = Test-Path -LiteralPath $target if ($exists -and -not $Force) { $status = "preserved_existing" } else { Draw-DraftAsset $target $product $field $status = if ($exists) { "regenerated_draft_png" } else { "generated_draft_png" } } $rows.Add([pscustomobject]@{ sku = $product.sku field = $field filename = $filename path = $target status = $status approvalStatus = "draft_needs_human_image_approval" }) } } $json = [pscustomobject]@{ generatedAt = $GeneratedAt assetRoot = $AssetRoot totalProductPdpAssets = $rows.Count generated = @($rows | Where-Object { $_.status -like "generated*" }).Count regenerated = @($rows | Where-Object { $_.status -like "regenerated*" }).Count preserved = @($rows | Where-Object { $_.status -eq "preserved_existing" }).Count caveat = "Draft PNGs are staging/review assets generated from current dashboard evidence. They are not human-approved final creative." rows = $rows } $json | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $OutRoot "draft_pdp_asset_generation.json") -Encoding UTF8 $rows | Export-Csv -LiteralPath (Join-Path $OutRoot "draft_pdp_asset_generation.csv") -NoTypeInformation -Encoding UTF8 Write-Host "Draft PDP assets represented: $($rows.Count)" Write-Host "Generated: $(@($rows | Where-Object { $_.status -eq 'generated_draft_png' }).Count)" Write-Host "Regenerated: $(@($rows | Where-Object { $_.status -eq 'regenerated_draft_png' }).Count)" Write-Host "Preserved existing: $(@($rows | Where-Object { $_.status -eq 'preserved_existing' }).Count)"