CI/CD Integration
Integrate pngx into your CI/CD pipelines for automated image processing, testing, and optimization.
GitHub Actions
Basic Image Processing
yaml
# .github/workflows/process-images.yml
name: Process Images
on:
push:
paths:
- 'images/**'
jobs:
process:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Process images
run: bun run process-images.ts
- name: Upload processed images
uses: actions/upload-artifact@v3
with:
name: processed-images
path: output/Image Optimization Pipeline
yaml
# .github/workflows/optimize.yml
name: Optimize PNG Images
on:
pull_request:
paths:
- '**/*.png'
jobs:
optimize:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- name: Install pngx
run: bun add pngx
- name: Optimize PNGs
run: |
bun run << 'EOF'
import { PNG } from 'pngx'
import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs'
import { join } from 'path'
function optimizePNG(path) {
const buffer = readFileSync(path)
const png = PNG.sync.read(buffer)
const optimized = PNG.sync.write(new PNG({
width: png.width,
height: png.height,
deflateLevel: 9,
filterType: -1,
}))
writeFileSync(path, optimized)
return { original: buffer.length, optimized: optimized.length }
}
function findPNGs(dir) {
const files = []
for (const entry of readdirSync(dir)) {
const path = join(dir, entry)
if (statSync(path).isDirectory()) {
files.push(...findPNGs(path))
} else if (entry.endsWith('.png')) {
files.push(path)
}
}
return files
}
const pngs = findPNGs('.')
let totalSaved = 0
for (const png of pngs) {
const { original, optimized } = optimizePNG(png)
totalSaved += original - optimized
console.log(`${png}: ${original} -> ${optimized} (${((1 - optimized/original) * 100).toFixed(1)}% reduction)`)
}
console.log(`Total saved: ${(totalSaved / 1024).toFixed(2)} KB`)
EOF
- name: Commit optimized images
run: |
git config user.name "GitHub Action"
git config user.email "action@github.com"
git add -A
git commit -m "Optimize PNG images" || exit 0
git pushVisual Regression Testing
yaml
# .github/workflows/visual-test.yml
name: Visual Regression
on:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Run visual tests
run: bun run visual-test.ts
- name: Upload diff images
if: failure()
uses: actions/upload-artifact@v3
with:
name: visual-diffs
path: test-output/diffs/GitLab CI
yaml
# .gitlab-ci.yml
image: oven/bun:latest
stages:
- process
- test
- deploy
process-images:
stage: process
script:
- bun install
- bun run process-images.ts
artifacts:
paths:
- output/
expire_in: 1 week
test-images:
stage: test
script:
- bun install
- bun run test-images.ts
artifacts:
paths:
- test-output/
when: on_failure
deploy-images:
stage: deploy
script:
- bun run deploy-images.ts
only:
- mainProcessing Scripts
Batch Optimization
typescript
// process-images.ts
import { PNG } from 'pngx'
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'
import { glob } from 'glob'
import { basename, join } from 'path'
interface ProcessResult {
file: string
original: number
optimized: number
saved: number
}
async function processImages(): Promise<ProcessResult[]> {
const results: ProcessResult[] = []
const inputDir = process.env.INPUT_DIR || 'images'
const outputDir = process.env.OUTPUT_DIR || 'output'
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true })
}
const files = await glob(`${inputDir}/**/*.png`)
for (const file of files) {
const buffer = readFileSync(file)
const png = PNG.sync.read(buffer)
const optimizedPng = new PNG({
width: png.width,
height: png.height,
deflateLevel: 9,
deflateStrategy: 3,
filterType: -1,
})
png.data.copy(optimizedPng.data)
const optimized = PNG.sync.write(optimizedPng)
const outputPath = join(outputDir, basename(file))
writeFileSync(outputPath, optimized)
results.push({
file,
original: buffer.length,
optimized: optimized.length,
saved: buffer.length - optimized.length,
})
}
return results
}
const results = await processImages()
// Generate report
const totalOriginal = results.reduce((sum, r) => sum + r.original, 0)
const totalOptimized = results.reduce((sum, r) => sum + r.optimized, 0)
console.log('Processing complete!')
console.log(`Files: ${results.length}`)
console.log(`Original size: ${(totalOriginal / 1024).toFixed(2)} KB`)
console.log(`Optimized size: ${(totalOptimized / 1024).toFixed(2)} KB`)
console.log(`Saved: ${((totalOriginal - totalOptimized) / 1024).toFixed(2)} KB (${((1 - totalOptimized / totalOriginal) * 100).toFixed(1)}%)`)Visual Comparison Testing
typescript
// visual-test.ts
import { PNG } from 'pngx'
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'
import { join } from 'path'
interface CompareResult {
match: boolean
diffPixels: number
diffPercent: number
}
function comparePNG(png1: PNG, png2: PNG): CompareResult {
if (png1.width !== png2.width || png1.height !== png2.height) {
return { match: false, diffPixels: -1, diffPercent: 100 }
}
let diffPixels = 0
const totalPixels = png1.width * png1.height
for (let i = 0; i < png1.data.length; i += 4) {
const diff =
Math.abs(png1.data[i] - png2.data[i]) +
Math.abs(png1.data[i + 1] - png2.data[i + 1]) +
Math.abs(png1.data[i + 2] - png2.data[i + 2]) +
Math.abs(png1.data[i + 3] - png2.data[i + 3])
if (diff > 0) {
diffPixels++
}
}
return {
match: diffPixels === 0,
diffPixels,
diffPercent: (diffPixels / totalPixels) * 100,
}
}
function createDiffImage(png1: PNG, png2: PNG): PNG {
const diff = new PNG({
width: png1.width,
height: png1.height,
})
for (let i = 0; i < png1.data.length; i += 4) {
const pixelDiff =
Math.abs(png1.data[i] - png2.data[i]) +
Math.abs(png1.data[i + 1] - png2.data[i + 1]) +
Math.abs(png1.data[i + 2] - png2.data[i + 2])
if (pixelDiff > 0) {
diff.data[i] = 255 // Red for differences
diff.data[i + 1] = 0
diff.data[i + 2] = 0
diff.data[i + 3] = 255
} else {
diff.data[i] = png1.data[i]
diff.data[i + 1] = png1.data[i + 1]
diff.data[i + 2] = png1.data[i + 2]
diff.data[i + 3] = 128 // Semi-transparent for matches
}
}
return diff
}
// Run tests
const baselineDir = 'test/baselines'
const actualDir = 'test/actual'
const diffDir = 'test-output/diffs'
if (!existsSync(diffDir)) {
mkdirSync(diffDir, { recursive: true })
}
let passed = 0
let failed = 0
const baselines = readdirSync(baselineDir).filter(f => f.endsWith('.png'))
for (const file of baselines) {
const baselinePath = join(baselineDir, file)
const actualPath = join(actualDir, file)
if (!existsSync(actualPath)) {
console.log(`MISSING: ${file}`)
failed++
continue
}
const baseline = PNG.sync.read(readFileSync(baselinePath))
const actual = PNG.sync.read(readFileSync(actualPath))
const result = comparePNG(baseline, actual)
if (result.match) {
console.log(`PASS: ${file}`)
passed++
} else {
console.log(`FAIL: ${file} (${result.diffPercent.toFixed(2)}% different)`)
const diffImage = createDiffImage(baseline, actual)
writeFileSync(join(diffDir, file), PNG.sync.write(diffImage))
failed++
}
}
console.log(`\nResults: ${passed} passed, ${failed} failed`)
process.exit(failed > 0 ? 1 : 0)Docker Integration
Dockerfile
dockerfile
FROM oven/bun:latest
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
CMD ["bun", "run", "process-images.ts"]Docker Compose
yaml
version: '3.8'
services:
image-processor:
build: .
volumes:
- ./input:/app/input
- ./output:/app/output
environment:
- INPUT_DIR=/app/input
- OUTPUT_DIR=/app/output
- COMPRESSION_LEVEL=9Environment Configuration
Configuration Script
typescript
// config.ts
interface Config {
inputDir: string
outputDir: string
compression: {
level: number
strategy: number
filter: number
}
skipCRC: boolean
}
export function loadConfig(): Config {
return {
inputDir: process.env.INPUT_DIR || 'images',
outputDir: process.env.OUTPUT_DIR || 'output',
compression: {
level: Number(process.env.COMPRESSION_LEVEL) || 9,
strategy: Number(process.env.COMPRESSION_STRATEGY) || 3,
filter: Number(process.env.FILTER_TYPE) || -1,
},
skipCRC: process.env.SKIP_CRC === 'true',
}
}Environment Variables
bash
# .env.ci
INPUT_DIR=./images
OUTPUT_DIR=./output
COMPRESSION_LEVEL=9
COMPRESSION_STRATEGY=3
FILTER_TYPE=-1
SKIP_CRC=trueBest Practices
- Cache Dependencies: Cache bun/npm dependencies between runs
- Parallel Processing: Use worker threads for batch operations
- Artifact Storage: Store processed images as artifacts
- Fail Fast: Exit early on critical errors
- Report Results: Generate clear summaries
Troubleshooting
Out of Memory
yaml
# Increase memory limit
- name: Process large images
run: bun run --bun process-images.ts
env:
NODE_OPTIONS: --max-old-space-size=4096Timeout Issues
yaml
# Increase timeout
- name: Long running process
run: bun run process-images.ts
timeout-minutes: 30Next Steps
- Performance - Optimize processing speed
- Custom Profiles - Reusable configurations
- Configuration - Detailed settings