- manifest.webmanifest + service worker (offline shell, /api network-only, asset stale-while-revalidate) - set icone (192/512 + maskable + apple-touch + favicon), generate zero-deps (pure Node+zlib) - tag PWA + viewport viewport-fit=cover (zoom mantenuto, WCAG 1.4.4) in 77 pagine HTML - AGID/WCAG 2.1 AA: touch target >=44px, orientamento/zoom liberi, safe-area standalone - generatori riproducibili: scripts/gen-pwa-icons.mjs + scripts/inject-pwa-head.mjs - bump version 1.14.1 -> 1.15.0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
488 lines
18 KiB
JavaScript
488 lines
18 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* gen-pwa-icons.mjs — NIS2 Agile PWA icon generator (dependency-free)
|
|
*
|
|
* Generates the full PWA icon set using ONLY Node v20 built-ins:
|
|
* - node:zlib (deflate for PNG IDAT)
|
|
* - node:fs (write files)
|
|
* - node:path (resolve paths)
|
|
*
|
|
* Design: Italy-blue (#0066CC) square tile, centered white shield
|
|
* (compliance/security silhouette) with a bold "negative" check mark
|
|
* cut out of the shield (rendered in #0066CC) so it stays readable
|
|
* down to 16px.
|
|
*
|
|
* Rendering: each icon is drawn at 4x (supersampling) into an RGBA
|
|
* buffer using hand-written primitives (rounded rect, scanline polygon
|
|
* fill), then box-downsampled to final size for smooth anti-aliased
|
|
* edges. PNG is encoded manually (8-bit, color type 6 RGBA).
|
|
*
|
|
* Run: cd /projects/nis2-agile && node scripts/gen-pwa-icons.mjs
|
|
*/
|
|
|
|
import zlib from 'node:zlib';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Paths
|
|
// ---------------------------------------------------------------------------
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const PROJECT_ROOT = path.resolve(__dirname, '..');
|
|
const ICONS_DIR = path.join(PROJECT_ROOT, 'public', 'assets', 'icons');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Palette
|
|
// ---------------------------------------------------------------------------
|
|
const BLUE = [0, 102, 204]; // #0066CC — Italy blue
|
|
const WHITE = [255, 255, 255];
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// RGBA canvas helpers
|
|
// ---------------------------------------------------------------------------
|
|
function makeCanvas(w, h, fill) {
|
|
// fill = [r,g,b,a] or null => transparent (0,0,0,0)
|
|
const buf = new Uint8Array(w * h * 4);
|
|
if (fill) {
|
|
for (let i = 0; i < w * h; i++) {
|
|
const o = i * 4;
|
|
buf[o] = fill[0];
|
|
buf[o + 1] = fill[1];
|
|
buf[o + 2] = fill[2];
|
|
buf[o + 3] = fill[3];
|
|
}
|
|
}
|
|
return { w, h, buf };
|
|
}
|
|
|
|
function setPx(cv, x, y, rgba) {
|
|
if (x < 0 || y < 0 || x >= cv.w || y >= cv.h) return;
|
|
const o = (y * cv.w + x) * 4;
|
|
cv.buf[o] = rgba[0];
|
|
cv.buf[o + 1] = rgba[1];
|
|
cv.buf[o + 2] = rgba[2];
|
|
cv.buf[o + 3] = rgba[3];
|
|
}
|
|
|
|
// Fill the whole canvas with an OPAQUE color.
|
|
function fillBackground(cv, rgb) {
|
|
for (let i = 0; i < cv.w * cv.h; i++) {
|
|
const o = i * 4;
|
|
cv.buf[o] = rgb[0];
|
|
cv.buf[o + 1] = rgb[1];
|
|
cv.buf[o + 2] = rgb[2];
|
|
cv.buf[o + 3] = 255;
|
|
}
|
|
}
|
|
|
|
// Filled rounded rectangle (opaque). r = corner radius in px (canvas space).
|
|
function fillRoundedRect(cv, x0, y0, x1, y1, r, rgb, alpha = 255) {
|
|
const minX = Math.max(0, Math.floor(x0));
|
|
const minY = Math.max(0, Math.floor(y0));
|
|
const maxX = Math.min(cv.w - 1, Math.ceil(x1));
|
|
const maxY = Math.min(cv.h - 1, Math.ceil(y1));
|
|
const rr = r * r;
|
|
for (let y = minY; y <= maxY; y++) {
|
|
for (let x = minX; x <= maxX; x++) {
|
|
const px = x + 0.5;
|
|
const py = y + 0.5;
|
|
let inside = true;
|
|
// corner clipping
|
|
if (px < x0 + r && py < y0 + r) {
|
|
const dx = px - (x0 + r), dy = py - (y0 + r);
|
|
inside = dx * dx + dy * dy <= rr;
|
|
} else if (px > x1 - r && py < y0 + r) {
|
|
const dx = px - (x1 - r), dy = py - (y0 + r);
|
|
inside = dx * dx + dy * dy <= rr;
|
|
} else if (px < x0 + r && py > y1 - r) {
|
|
const dx = px - (x0 + r), dy = py - (y1 - r);
|
|
inside = dx * dx + dy * dy <= rr;
|
|
} else if (px > x1 - r && py > y1 - r) {
|
|
const dx = px - (x1 - r), dy = py - (y1 - r);
|
|
inside = dx * dx + dy * dy <= rr;
|
|
}
|
|
if (inside) setPx(cv, x, y, [rgb[0], rgb[1], rgb[2], alpha]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Scanline polygon fill. pts = [[x,y],...] in canvas space. Even-odd rule.
|
|
function fillPolygon(cv, pts, rgb, alpha = 255) {
|
|
let minY = Infinity, maxY = -Infinity;
|
|
for (const p of pts) {
|
|
if (p[1] < minY) minY = p[1];
|
|
if (p[1] > maxY) maxY = p[1];
|
|
}
|
|
minY = Math.max(0, Math.floor(minY));
|
|
maxY = Math.min(cv.h - 1, Math.ceil(maxY));
|
|
const n = pts.length;
|
|
for (let y = minY; y <= maxY; y++) {
|
|
const yc = y + 0.5;
|
|
const xs = [];
|
|
for (let i = 0; i < n; i++) {
|
|
const a = pts[i];
|
|
const b = pts[(i + 1) % n];
|
|
const ay = a[1], by = b[1];
|
|
if ((ay <= yc && by > yc) || (by <= yc && ay > yc)) {
|
|
const t = (yc - ay) / (by - ay);
|
|
xs.push(a[0] + t * (b[0] - a[0]));
|
|
}
|
|
}
|
|
xs.sort((p, q) => p - q);
|
|
for (let i = 0; i + 1 < xs.length; i += 2) {
|
|
const xStart = Math.max(0, Math.ceil(xs[i] - 0.5));
|
|
const xEnd = Math.min(cv.w - 1, Math.floor(xs[i + 1] - 0.5));
|
|
for (let x = xStart; x <= xEnd; x++) setPx(cv, x, y, [rgb[0], rgb[1], rgb[2], alpha]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shield + check geometry
|
|
// ---------------------------------------------------------------------------
|
|
// All geometry is expressed in a normalized 0..1 box, then scaled into the
|
|
// target region (cx,cy = center, half = half-size of the content square).
|
|
|
|
// Build shield outline polygon (heraldic shield: flat top, rounded shoulders
|
|
// approximated by points, tapering to a point at the bottom).
|
|
function shieldPolygon(cx, cy, half) {
|
|
// Normalized shield within a square: x in [-0.5,0.5], y in [-0.6,0.6]
|
|
// top edge slightly inset, sides curve down, bottom comes to a point.
|
|
const W = half * 0.92; // half width
|
|
const Htop = -half * 1.02;
|
|
const pts = [];
|
|
// Top edge
|
|
pts.push([cx - W, cy + Htop]);
|
|
pts.push([cx + W, cy + Htop]);
|
|
// Right side going down with a gentle inward curve, then to the point.
|
|
// Approximate the right & bottom with sampled points along a parametric path.
|
|
const right = [
|
|
[cx + W, cy + Htop],
|
|
[cx + W * 0.99, cy - half * 0.35],
|
|
[cx + W * 0.92, cy + half * 0.05],
|
|
[cx + W * 0.74, cy + half * 0.45],
|
|
[cx + W * 0.42, cy + half * 0.85],
|
|
[cx + 0, cy + half * 1.12], // bottom point
|
|
];
|
|
const left = [
|
|
[cx - W * 0.42, cy + half * 0.85],
|
|
[cx - W * 0.74, cy + half * 0.45],
|
|
[cx - W * 0.92, cy + half * 0.05],
|
|
[cx - W * 0.99, cy - half * 0.35],
|
|
[cx - W, cy + Htop],
|
|
];
|
|
// Compose: top-left -> top-right -> right path -> bottom -> left path
|
|
const poly = [];
|
|
poly.push([cx - W, cy + Htop]);
|
|
poly.push([cx + W, cy + Htop]);
|
|
for (let i = 1; i < right.length; i++) poly.push(right[i]);
|
|
for (let i = 0; i < left.length; i++) poly.push(left[i]);
|
|
return poly;
|
|
}
|
|
|
|
// Build a bold check-mark polygon (thick stroke as a filled hexagon-ish shape).
|
|
// Centered roughly in the shield. `s` scales overall size, `t` is stroke thickness.
|
|
function checkPolygon(cx, cy, s, t) {
|
|
// Check has a short arm (down-left) and a long arm (up-right).
|
|
// Define center vertex (bottom of the V), short tip, long tip.
|
|
const vx = cx - s * 0.10, vy = cy + s * 0.34; // inner corner (bottom)
|
|
const s1x = cx - s * 0.46, s1y = cy + s * 0.02; // short arm tip
|
|
const l1x = cx + s * 0.50, l1y = cy - s * 0.42; // long arm tip
|
|
// Build a thick stroke by offsetting perpendicular to each segment.
|
|
// Segment A: short tip -> inner corner
|
|
// Segment B: inner corner -> long tip
|
|
const half = t / 2;
|
|
|
|
function unit(ax, ay, bx, by) {
|
|
const dx = bx - ax, dy = by - ay;
|
|
const L = Math.hypot(dx, dy) || 1;
|
|
return [dx / L, dy / L];
|
|
}
|
|
function perp(u) { return [-u[1], u[0]]; }
|
|
|
|
const uA = unit(s1x, s1y, vx, vy);
|
|
const uB = unit(vx, vy, l1x, l1y);
|
|
const pA = perp(uA);
|
|
const pB = perp(uB);
|
|
|
|
// Outer & inner offsets for each arm; assemble as a single polygon outline.
|
|
// Order: short tip outer -> corner outer -> long tip outer -> long tip inner
|
|
// -> corner inner -> short tip inner
|
|
const poly = [
|
|
[s1x + pA[0] * half, s1y + pA[1] * half], // short tip outer
|
|
[vx + pA[0] * half, vy + pA[1] * half], // corner outer (A side)
|
|
[vx + pB[0] * half, vy + pB[1] * half], // corner outer (B side)
|
|
[l1x + pB[0] * half, l1y + pB[1] * half], // long tip outer
|
|
[l1x - pB[0] * half, l1y - pB[1] * half], // long tip inner
|
|
[vx - pB[0] * half, vy - pB[1] * half], // corner inner (B side)
|
|
[vx - pA[0] * half, vy - pA[1] * half], // corner inner (A side)
|
|
[s1x - pA[0] * half, s1y - pA[1] * half], // short tip inner
|
|
];
|
|
return poly;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Draw the NIS2 emblem (shield + check) into a canvas region.
|
|
// cx,cy: center; half: half content size; simplified: bolder geometry for tiny sizes
|
|
// ---------------------------------------------------------------------------
|
|
function drawEmblem(cv, cx, cy, half, simplified = false) {
|
|
// White shield
|
|
const shield = shieldPolygon(cx, cy, half);
|
|
fillPolygon(cv, shield, WHITE, 255);
|
|
|
|
// Negative blue check inside the shield.
|
|
const checkScale = half * (simplified ? 1.18 : 1.05);
|
|
const stroke = half * (simplified ? 0.32 : 0.24);
|
|
const check = checkPolygon(cx, cy - half * 0.02, checkScale, stroke);
|
|
fillPolygon(cv, check, BLUE, 255);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Box downsample (supersampled -> final). factor must divide dimensions.
|
|
// ---------------------------------------------------------------------------
|
|
function downsample(cv, factor) {
|
|
const W = cv.w / factor;
|
|
const H = cv.h / factor;
|
|
const out = makeCanvas(W, H, null);
|
|
const f2 = factor * factor;
|
|
for (let y = 0; y < H; y++) {
|
|
for (let x = 0; x < W; x++) {
|
|
let r = 0, g = 0, b = 0, a = 0;
|
|
for (let sy = 0; sy < factor; sy++) {
|
|
for (let sx = 0; sx < factor; sx++) {
|
|
const o = ((y * factor + sy) * cv.w + (x * factor + sx)) * 4;
|
|
const sa = cv.buf[o + 3];
|
|
// premultiply for correct edge blending
|
|
r += cv.buf[o] * sa;
|
|
g += cv.buf[o + 1] * sa;
|
|
b += cv.buf[o + 2] * sa;
|
|
a += sa;
|
|
}
|
|
}
|
|
const oo = (y * W + x) * 4;
|
|
if (a === 0) {
|
|
out.buf[oo] = 0; out.buf[oo + 1] = 0; out.buf[oo + 2] = 0; out.buf[oo + 3] = 0;
|
|
} else {
|
|
out.buf[oo] = Math.round(r / a);
|
|
out.buf[oo + 1] = Math.round(g / a);
|
|
out.buf[oo + 2] = Math.round(b / a);
|
|
out.buf[oo + 3] = Math.round(a / f2);
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// PNG encoding (manual, 8-bit RGBA, color type 6)
|
|
// ---------------------------------------------------------------------------
|
|
const PNG_SIG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
|
|
// CRC32 (standard polynomial 0xEDB88320)
|
|
const CRC_TABLE = (() => {
|
|
const t = new Uint32Array(256);
|
|
for (let n = 0; n < 256; n++) {
|
|
let c = n;
|
|
for (let k = 0; k < 8; k++) {
|
|
c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
|
|
}
|
|
t[n] = c >>> 0;
|
|
}
|
|
return t;
|
|
})();
|
|
|
|
function crc32(buf) {
|
|
let c = 0xffffffff;
|
|
for (let i = 0; i < buf.length; i++) {
|
|
c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
}
|
|
return (c ^ 0xffffffff) >>> 0;
|
|
}
|
|
|
|
function chunk(type, data) {
|
|
const len = Buffer.alloc(4);
|
|
len.writeUInt32BE(data.length, 0);
|
|
const typeBuf = Buffer.from(type, 'ascii');
|
|
const body = Buffer.concat([typeBuf, data]);
|
|
const crc = Buffer.alloc(4);
|
|
crc.writeUInt32BE(crc32(body), 0);
|
|
return Buffer.concat([len, body, crc]);
|
|
}
|
|
|
|
function encodePNG(cv) {
|
|
const { w, h, buf } = cv;
|
|
// IHDR
|
|
const ihdr = Buffer.alloc(13);
|
|
ihdr.writeUInt32BE(w, 0);
|
|
ihdr.writeUInt32BE(h, 4);
|
|
ihdr[8] = 8; // bit depth
|
|
ihdr[9] = 6; // color type RGBA
|
|
ihdr[10] = 0; // compression
|
|
ihdr[11] = 0; // filter
|
|
ihdr[12] = 0; // interlace
|
|
|
|
// Filtered raw data: each scanline prefixed with filter byte 0
|
|
const stride = w * 4;
|
|
const raw = Buffer.alloc((stride + 1) * h);
|
|
for (let y = 0; y < h; y++) {
|
|
raw[y * (stride + 1)] = 0; // filter: none
|
|
buf.subarray(y * stride, y * stride + stride)
|
|
.forEach ? null : null; // (Uint8Array subarray copy below)
|
|
const srcStart = y * stride;
|
|
raw.set(buf.subarray(srcStart, srcStart + stride), y * (stride + 1) + 1);
|
|
}
|
|
const idat = zlib.deflateSync(raw, { level: 9 });
|
|
|
|
return Buffer.concat([
|
|
PNG_SIG,
|
|
chunk('IHDR', ihdr),
|
|
chunk('IDAT', idat),
|
|
chunk('IEND', Buffer.alloc(0)),
|
|
]);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Icon builders
|
|
// ---------------------------------------------------------------------------
|
|
const SS = 4; // supersampling factor
|
|
|
|
// Standard (rounded tile) icon: transparent corners, rounded blue tile,
|
|
// margin ~8%, centered emblem.
|
|
function buildStandard(size, simplified = false) {
|
|
const W = size * SS;
|
|
const cv = makeCanvas(W, W, null); // transparent background
|
|
const margin = W * 0.08;
|
|
const radius = W * 0.18;
|
|
fillRoundedRect(cv, margin, margin, W - margin, W - margin, radius, BLUE, 255);
|
|
const cx = W / 2, cy = W / 2;
|
|
// emblem content sized to ~ inner tile
|
|
const half = (W - 2 * margin) * 0.30;
|
|
drawEmblem(cv, cx, cy, half, simplified);
|
|
return downsample(cv, SS);
|
|
}
|
|
|
|
// Maskable icon: FULL-BLEED blue, square corners, emblem inside the central
|
|
// 80% safe-zone.
|
|
function buildMaskable(size) {
|
|
const W = size * SS;
|
|
const cv = makeCanvas(W, W, null);
|
|
fillBackground(cv, BLUE); // full bleed, opaque
|
|
const cx = W / 2, cy = W / 2;
|
|
// safe zone is central 80% => content sized so it stays inside 0.8*W box
|
|
const half = W * 0.5 * 0.80 * 0.62; // ~31% of half-canvas
|
|
drawEmblem(cv, cx, cy, half, false);
|
|
return downsample(cv, SS);
|
|
}
|
|
|
|
// Apple touch icon: fully OPAQUE blue background, square corners, emblem.
|
|
function buildAppleTouch(size) {
|
|
const W = size * SS;
|
|
const cv = makeCanvas(W, W, null);
|
|
fillBackground(cv, BLUE); // opaque, no transparency
|
|
const cx = W / 2, cy = W / 2;
|
|
const half = W * 0.30;
|
|
drawEmblem(cv, cx, cy, half, false);
|
|
return downsample(cv, SS);
|
|
}
|
|
|
|
// Favicon: rounded blue tile + bolder simplified emblem for legibility.
|
|
function buildFavicon(size) {
|
|
const W = size * SS;
|
|
const cv = makeCanvas(W, W, null);
|
|
const margin = W * 0.04;
|
|
const radius = W * 0.18;
|
|
fillRoundedRect(cv, margin, margin, W - margin, W - margin, radius, BLUE, 255);
|
|
const cx = W / 2, cy = W / 2;
|
|
const half = W * 0.34;
|
|
drawEmblem(cv, cx, cy, half, true); // simplified/bolder
|
|
return downsample(cv, SS);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SVG master (same design, vector)
|
|
// ---------------------------------------------------------------------------
|
|
function buildSVG() {
|
|
// Express the same geometry in a 512 viewBox.
|
|
const cx = 256, cy = 256, half = 512 * 0.30; // 153.6
|
|
const shield = shieldPolygon(cx, cy, half)
|
|
.map(p => `${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' ');
|
|
const check = checkPolygon(cx, cy - half * 0.02, half * 1.05, half * 0.24)
|
|
.map(p => `${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' ');
|
|
const margin = 512 * 0.08;
|
|
const r = 512 * 0.18;
|
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-label="NIS2 Agile">
|
|
<title>NIS2 Agile</title>
|
|
<!-- Italy-blue rounded tile -->
|
|
<rect x="${margin}" y="${margin}" width="${512 - 2 * margin}" height="${512 - 2 * margin}" rx="${r}" ry="${r}" fill="#0066CC"/>
|
|
<!-- White shield -->
|
|
<polygon points="${shield}" fill="#FFFFFF"/>
|
|
<!-- Negative blue check -->
|
|
<polygon points="${check}" fill="#0066CC"/>
|
|
</svg>
|
|
`;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main
|
|
// ---------------------------------------------------------------------------
|
|
function main() {
|
|
fs.mkdirSync(ICONS_DIR, { recursive: true });
|
|
|
|
const outputs = [];
|
|
|
|
function write(name, cv) {
|
|
const png = encodePNG(cv);
|
|
const fp = path.join(ICONS_DIR, name);
|
|
fs.writeFileSync(fp, png);
|
|
outputs.push({ name, bytes: png.length, w: cv.w, h: cv.h });
|
|
}
|
|
|
|
write('icon-192.png', buildStandard(192));
|
|
write('icon-512.png', buildStandard(512));
|
|
write('icon-192-maskable.png', buildMaskable(192));
|
|
write('icon-512-maskable.png', buildMaskable(512));
|
|
write('apple-touch-icon.png', buildAppleTouch(180));
|
|
write('favicon-32.png', buildFavicon(32));
|
|
write('favicon-16.png', buildFavicon(16));
|
|
|
|
// SVG master
|
|
const svg = buildSVG();
|
|
fs.writeFileSync(path.join(ICONS_DIR, 'icon.svg'), svg);
|
|
|
|
// ---- Validation: re-read each PNG, verify signature + IHDR dims ----
|
|
console.log('\n=== NIS2 PWA Icon Generation ===');
|
|
console.log('Output dir:', ICONS_DIR);
|
|
console.log('\nFile | Bytes | Declared | Read(IHDR) | SIG');
|
|
console.log('---------------------------|--------|----------|------------|----');
|
|
|
|
let allOk = true;
|
|
for (const o of outputs) {
|
|
const data = fs.readFileSync(path.join(ICONS_DIR, o.name));
|
|
const sigOk =
|
|
data.length >= 8 &&
|
|
data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47 &&
|
|
data[4] === 0x0d && data[5] === 0x0a && data[6] === 0x1a && data[7] === 0x0a;
|
|
// IHDR starts at byte 16 (8 sig + 4 len + 4 'IHDR'); width@16, height@20
|
|
const rw = data.readUInt32BE(16);
|
|
const rh = data.readUInt32BE(20);
|
|
const nonEmpty = data.length > 0;
|
|
const ok = sigOk && nonEmpty && rw === o.w && rh === o.h;
|
|
if (!ok) allOk = false;
|
|
console.log(
|
|
`${o.name.padEnd(26)} | ${String(o.bytes).padStart(6)} | ${(o.w + 'x' + o.h).padStart(8)} | ${(rw + 'x' + rh).padStart(10)} | ${ok ? 'OK' : 'KO'}`
|
|
);
|
|
}
|
|
|
|
// SVG report
|
|
const svgStat = fs.statSync(path.join(ICONS_DIR, 'icon.svg'));
|
|
console.log(`${'icon.svg'.padEnd(26)} | ${String(svgStat.size).padStart(6)} | ${'vector'.padStart(8)} | ${'-'.padStart(10)} | ${svgStat.size > 0 ? 'OK' : 'KO'}`);
|
|
|
|
console.log('\nValidation:', allOk ? 'ALL PNG OK' : 'FAILURES PRESENT');
|
|
if (!allOk) process.exitCode = 1;
|
|
}
|
|
|
|
main();
|