feat: initial mycro-zine generator toolkit

- Single-page print layout (2x4 grid) for 8-page zines
- Prompt templates for AI content/image generation
- Example Undernet zine pages
- Support for US Letter and A4 paper sizes
- CLI and programmatic API

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-12-15 18:37:45 -05:00
commit d8b5c2cd88
15 changed files with 1266 additions and 0 deletions

27
.gitignore vendored Normal file
View File

@ -0,0 +1,27 @@
# Dependencies
node_modules/
# Output directory (generated files)
output/
# Environment
.env
.env.local
# OS
.DS_Store
Thumbs.db
# IDE
.idea/
.vscode/
*.swp
*.swo
# Logs
*.log
npm-debug.log*
# Build artifacts
dist/
build/

139
README.md Normal file
View File

@ -0,0 +1,139 @@
# MycroZine
A toolkit for creating print-ready **mycro-zines** - 8-page mini folded zines that fit on a single 8.5" x 11" sheet.
## What is a MycroZine?
A mycro-zine is a tiny, foldable magazine made from a single sheet of paper. When folded correctly, it creates an 8-page booklet perfect for:
- Punk zines and manifestos
- Educational mini-guides
- Event programs
- DIY instructions
- Art projects
## Features
- **Single-page print layout**: All 8 pages arranged on one 8.5" x 11" sheet (2 cols x 4 rows)
- **High-resolution output**: 300 DPI for crisp printing
- **Prompt templates**: Ready-to-use prompts for AI content/image generation
- **Multiple styles**: punk-zine, minimal, collage, retro, academic
- **US Letter & A4 support**: Works with common paper sizes
## Installation
```bash
npm install
```
## Usage
### CLI - Create Print Layout
```bash
# Using example pages
npm run example
# Or specify your own 8 pages
node src/layout.mjs page1.png page2.png page3.png page4.png page5.png page6.png page7.png page8.png
# With custom output path
node src/layout.mjs page1.png ... page8.png --output my_zine_print.png
```
### Programmatic API
```javascript
import { createPrintLayout } from 'mycro-zine';
// Create print-ready layout from 8 page images
await createPrintLayout({
pages: [
'page1.png', 'page2.png', 'page3.png', 'page4.png',
'page5.png', 'page6.png', 'page7.png', 'page8.png'
],
outputPath: 'my_zine_print.png',
background: '#ffffff'
});
```
### Prompt Templates (for AI generation)
```javascript
import { getContentOutlinePrompt, getImagePrompt, STYLES, TONES } from 'mycro-zine/prompts';
// Generate content outline prompt
const outlinePrompt = getContentOutlinePrompt({
topic: 'The Undernet',
style: 'punk-zine',
tone: 'rebellious',
sourceContent: 'Reference text here...'
});
// Generate image prompt for a page
const imagePrompt = getImagePrompt({
pageNumber: 1,
zineTopic: 'The Undernet',
pageOutline: {
title: 'THE UNDERNET',
keyPoints: ['Own your data', 'Run local servers'],
hashtags: ['#DataSovereignty', '#Mycopunk'],
imagePrompt: 'Bold cover with mycelial network imagery...'
},
style: 'punk-zine'
});
```
## Print Layout
The output is a single PNG image with all 8 pages arranged in reading order:
```
┌─────────────┬─────────────┐
│ Page 1 │ Page 2 │ Row 1
├─────────────┼─────────────┤
│ Page 3 │ Page 4 │ Row 2
├─────────────┼─────────────┤
│ Page 5 │ Page 6 │ Row 3
├─────────────┼─────────────┤
│ Page 7 │ Page 8 │ Row 4
└─────────────┴─────────────┘
Panel size: 4.25" x 2.75" (~10.8cm x 7cm)
Total: 8.5" x 11" at 300 DPI (2550 x 3300 pixels)
```
## Folding Instructions
After printing, fold your zine:
1. **Accordion fold**: Fold the paper in half horizontally (hamburger style)
2. **Fold again**: Fold in half vertically (hotdog style)
3. **One more fold**: Fold in half again
4. **Cut center**: Unfold completely, cut a slit along the center fold
5. **Push and fold**: Push through the center to create the booklet
## Examples
See the `examples/undernet/` directory for a complete 8-page zine about The Undernet project.
## Styles
| Style | Description |
|-------|-------------|
| `punk-zine` | Xerox texture, high contrast B&W, DIY collage, hand-drawn typography |
| `minimal` | Clean lines, white space, modern sans-serif, subtle gradients |
| `collage` | Layered imagery, mixed media textures, vintage photographs |
| `retro` | 1970s aesthetic, earth tones, groovy typography, halftone patterns |
| `academic` | Diagram-heavy, annotated illustrations, infographic elements |
## Integration with Gemini MCP
This library is designed to work with the [Gemini MCP Server](https://github.com/jeffemmett/gemini-mcp) for AI-powered content and image generation:
1. Use `getContentOutlinePrompt()` with `gemini_generate` for zine planning
2. Use `getImagePrompt()` with `gemini_generate_image` for page creation
3. Use `createPrintLayout()` to assemble the final print-ready file
## License
MIT

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

566
package-lock.json generated Normal file
View File

@ -0,0 +1,566 @@
{
"name": "mycro-zine",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mycro-zine",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"sharp": "^0.34.5"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
"integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@img/colour": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
"cpu": [
"ppc64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
"cpu": [
"riscv64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
"cpu": [
"s390x"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.4"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
"cpu": [
"ppc64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
"cpu": [
"riscv64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
"cpu": [
"s390x"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.7.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.2",
"semver": "^7.7.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.5",
"@img/sharp-darwin-x64": "0.34.5",
"@img/sharp-libvips-darwin-arm64": "1.2.4",
"@img/sharp-libvips-darwin-x64": "1.2.4",
"@img/sharp-libvips-linux-arm": "1.2.4",
"@img/sharp-libvips-linux-arm64": "1.2.4",
"@img/sharp-libvips-linux-ppc64": "1.2.4",
"@img/sharp-libvips-linux-riscv64": "1.2.4",
"@img/sharp-libvips-linux-s390x": "1.2.4",
"@img/sharp-libvips-linux-x64": "1.2.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
"@img/sharp-linux-arm": "0.34.5",
"@img/sharp-linux-arm64": "0.34.5",
"@img/sharp-linux-ppc64": "0.34.5",
"@img/sharp-linux-riscv64": "0.34.5",
"@img/sharp-linux-s390x": "0.34.5",
"@img/sharp-linux-x64": "0.34.5",
"@img/sharp-linuxmusl-arm64": "0.34.5",
"@img/sharp-linuxmusl-x64": "0.34.5",
"@img/sharp-wasm32": "0.34.5",
"@img/sharp-win32-arm64": "0.34.5",
"@img/sharp-win32-ia32": "0.34.5",
"@img/sharp-win32-x64": "0.34.5"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD",
"optional": true
}
}
}

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "mycro-zine",
"version": "1.0.0",
"description": "8-page mini-zine generator utilities for print-ready zine layouts",
"type": "module",
"main": "src/index.mjs",
"exports": {
".": "./src/index.mjs",
"./layout": "./src/layout.mjs",
"./prompts": "./src/prompts.mjs"
},
"scripts": {
"layout": "node src/layout.mjs",
"example": "node src/layout.mjs examples/undernet/undernet_zine_p1_cover.png examples/undernet/undernet_zine_p2_what.png examples/undernet/undernet_zine_p3_metacelium.png examples/undernet/undernet_zine_p4_privacy.png examples/undernet/undernet_zine_p5_threepunks.png examples/undernet/undernet_zine_p6_techstack.png examples/undernet/undernet_zine_p7_philosophy.png examples/undernet/undernet_zine_p8_cta.png"
},
"keywords": [
"zine",
"mycrozine",
"mini-zine",
"print",
"layout",
"punk",
"diy",
"generator"
],
"author": "Jeff Emmett",
"license": "MIT",
"dependencies": {
"sharp": "^0.34.5"
},
"engines": {
"node": ">=18.0.0"
},
"repository": {
"type": "git",
"url": "git@gitea.jeffemmett.com:jeffemmett/mycro-zine.git"
}
}

130
src/index.mjs Normal file
View File

@ -0,0 +1,130 @@
/**
* MycroZine - 8-page mini-zine generator utilities
*
* A toolkit for creating print-ready mycro-zines (8-page mini folded zines).
*
* @module mycro-zine
*/
export { createPrintLayout } from './layout.mjs';
export {
STYLES,
TONES,
PAGE_TEMPLATES,
ZINE_STRUCTURES,
getContentOutlinePrompt,
getImagePrompt,
getIdeationPrompt
} from './prompts.mjs';
/**
* Zine configuration defaults
*/
export const DEFAULTS = {
style: 'punk-zine',
tone: 'rebellious',
paperFormat: 'letter', // US Letter 8.5" x 11"
dpi: 300,
pageCount: 8
};
/**
* Page dimensions in pixels at 300 DPI
*/
export const DIMENSIONS = {
letter: {
width: 2550, // 8.5" x 300
height: 3300, // 11" x 300
panelWidth: 1275, // width / 2 cols
panelHeight: 825, // height / 4 rows
panelCols: 2,
panelRows: 4
},
a4: {
width: 2480, // 210mm at 300 DPI
height: 3508, // 297mm at 300 DPI
panelWidth: 1240,
panelHeight: 877,
panelCols: 2,
panelRows: 4
}
};
/**
* Validate a zine configuration
*
* @param {Object} config - Zine configuration to validate
* @returns {{ valid: boolean, errors: string[] }}
*/
export function validateConfig(config) {
const errors = [];
if (!config.topic || typeof config.topic !== 'string') {
errors.push('Topic is required and must be a string');
}
if (config.style && !['punk-zine', 'minimal', 'collage', 'retro', 'academic'].includes(config.style)) {
errors.push(`Invalid style: ${config.style}`);
}
if (config.tone && !['rebellious', 'playful', 'informative', 'poetic'].includes(config.tone)) {
errors.push(`Invalid tone: ${config.tone}`);
}
if (config.paperFormat && !['letter', 'a4'].includes(config.paperFormat)) {
errors.push(`Invalid paper format: ${config.paperFormat}`);
}
if (config.pages && (!Array.isArray(config.pages) || config.pages.length !== 8)) {
errors.push('Pages must be an array of exactly 8 items');
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Create a new zine configuration
*
* @param {Object} options
* @param {string} options.topic - Main topic/theme
* @param {string} [options.title] - Zine title (generated from topic if not provided)
* @param {string} [options.style='punk-zine'] - Visual style
* @param {string} [options.tone='rebellious'] - Content tone
* @param {string} [options.paperFormat='letter'] - Paper format
* @param {string[]} [options.sourceUrls] - Reference URLs
* @returns {Object} Zine configuration object
*/
export function createZineConfig(options) {
const {
topic,
title = topic.toUpperCase(),
style = DEFAULTS.style,
tone = DEFAULTS.tone,
paperFormat = DEFAULTS.paperFormat,
sourceUrls = []
} = options;
const config = {
id: `zine_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
topic,
title,
style,
tone,
paperFormat,
sourceUrls,
createdAt: Date.now(),
pages: [],
outline: null,
status: 'draft'
};
const validation = validateConfig(config);
if (!validation.valid) {
throw new Error(`Invalid config: ${validation.errors.join(', ')}`);
}
return config;
}

159
src/layout.mjs Normal file
View File

@ -0,0 +1,159 @@
/**
* MycroZine Layout Generator
*
* Creates a print-ready layout with all 8 pages on a single 8.5" x 11" sheet.
* Layout: 2 columns x 4 rows (reading order: left-to-right, top-to-bottom)
*
* Panel size: 4.25" x 2.75" (~10.8cm x 7cm)
*/
import sharp from 'sharp';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs/promises';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// 8.5" x 11" at 300 DPI = 2550 x 3300 pixels
const PAGE_WIDTH = 2550;
const PAGE_HEIGHT = 3300;
// Layout configuration: 2 columns x 4 rows
const COLS = 2;
const ROWS = 4;
const PANEL_WIDTH = Math.floor(PAGE_WIDTH / COLS); // 1275 pixels
const PANEL_HEIGHT = Math.floor(PAGE_HEIGHT / ROWS); // 825 pixels
/**
* Create a print-ready zine layout from 8 page images
*
* @param {Object} options - Layout options
* @param {string[]} options.pages - Array of 8 page image paths (in order)
* @param {string} [options.outputPath] - Output file path (default: output/mycrozine_print.png)
* @param {string} [options.background] - Background color (default: '#ffffff')
* @returns {Promise<string>} - Path to generated print layout
*/
export async function createPrintLayout(options) {
const {
pages,
outputPath = path.join(__dirname, '..', 'output', 'mycrozine_print.png'),
background = '#ffffff'
} = options;
if (!pages || pages.length !== 8) {
throw new Error('Exactly 8 page images are required');
}
// Ensure output directory exists
await fs.mkdir(path.dirname(outputPath), { recursive: true });
// Load and resize all pages to panel size
const resizedPages = await Promise.all(
pages.map(async (pagePath) => {
return sharp(pagePath)
.resize(PANEL_WIDTH, PANEL_HEIGHT, {
fit: 'contain',
background
})
.toBuffer();
})
);
// Build composite array for all 8 pages in reading order
const compositeImages = [];
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const pageIndex = row * COLS + col; // 0-7
compositeImages.push({
input: resizedPages[pageIndex],
left: col * PANEL_WIDTH,
top: row * PANEL_HEIGHT
});
}
}
// Create the final composite image
await sharp({
create: {
width: PAGE_WIDTH,
height: PAGE_HEIGHT,
channels: 3,
background
}
})
.composite(compositeImages)
.png()
.toFile(outputPath);
console.log(`Created print layout: ${outputPath}`);
console.log(` Dimensions: ${PAGE_WIDTH}x${PAGE_HEIGHT} pixels (8.5"x11" @ 300 DPI)`);
console.log(` Panel size: ${PANEL_WIDTH}x${PANEL_HEIGHT} pixels (${COLS}x${ROWS} grid)`);
return outputPath;
}
/**
* CLI entry point
* Usage: node layout.mjs <page1> <page2> ... <page8> [--output <path>]
*/
async function main() {
const args = process.argv.slice(2);
// Parse arguments
let pages = [];
let outputPath = null;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--output' || args[i] === '-o') {
outputPath = args[++i];
} else if (!args[i].startsWith('-')) {
pages.push(args[i]);
}
}
if (pages.length === 0) {
// Default: look for undernet_zine_p*.png in examples
const exampleDir = path.join(__dirname, '..', 'examples', 'undernet');
try {
const files = await fs.readdir(exampleDir);
pages = files
.filter(f => f.match(/undernet_zine_p\d\.png/))
.sort()
.map(f => path.join(exampleDir, f));
if (pages.length === 0) {
console.log('No page images found. Usage:');
console.log(' node layout.mjs page1.png page2.png ... page8.png [--output path]');
console.log(' Or place 8 pages named undernet_zine_p1.png through p8.png in examples/undernet/');
process.exit(1);
}
} catch (e) {
console.log('Usage: node layout.mjs page1.png page2.png ... page8.png [--output path]');
process.exit(1);
}
}
if (pages.length !== 8) {
console.error(`Error: Expected 8 pages, got ${pages.length}`);
process.exit(1);
}
const options = { pages };
if (outputPath) {
options.outputPath = outputPath;
}
try {
await createPrintLayout(options);
} catch (error) {
console.error('Error creating layout:', error.message);
process.exit(1);
}
}
// Run CLI if executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
export default createPrintLayout;

207
src/prompts.mjs Normal file
View File

@ -0,0 +1,207 @@
/**
* MycroZine Prompt Templates
*
* Templates for generating zine content outlines and image prompts.
* Designed for use with Gemini MCP tools.
*/
/**
* Available zine styles
*/
export const STYLES = {
'punk-zine': 'xerox texture, high contrast black and white with accent color highlights, DIY cut-and-paste collage, hand-drawn typography, rough edges, rebellious feel',
'minimal': 'clean lines, lots of white space, simple geometric shapes, modern sans-serif typography, subtle gradients',
'collage': 'layered imagery, mixed media textures, overlapping elements, vintage photographs, torn paper edges',
'retro': '1970s aesthetic, warm earth tones, groovy typography, halftone patterns, nostalgic imagery',
'academic': 'diagram-heavy, annotated illustrations, serif typography, reference-style layout, infographic elements'
};
/**
* Available tones
*/
export const TONES = {
'rebellious': 'defiant, anti-establishment, punk attitude, call to action, questioning authority',
'playful': 'whimsical, fun, light-hearted, humorous, engaging, accessible',
'informative': 'educational, clear explanations, factual, well-structured, easy to understand',
'poetic': 'lyrical, metaphorical, evocative imagery, emotional resonance, artistic expression'
};
/**
* Generate a content outline prompt for an 8-page zine
*
* @param {Object} options
* @param {string} options.topic - Main topic/theme
* @param {string} [options.style='punk-zine'] - Visual style
* @param {string} [options.tone='rebellious'] - Tone of content
* @param {string} [options.sourceContent] - Optional reference content
* @returns {string} Prompt for content outline generation
*/
export function getContentOutlinePrompt({ topic, style = 'punk-zine', tone = 'rebellious', sourceContent = null }) {
return `You are creating an 8-page mycro-zine (mini folded zine) on the topic: ${topic}
Style: ${style} | Tone: ${tone}
${sourceContent ? `Reference content:\n${sourceContent}\n` : ''}
Generate a JSON outline for 8 pages:
Page 1 (Cover): Bold title, subtitle, visual hook
Pages 2-7 (Content): Key concepts with emoji-fied, memetic explanations, hashtags
Page 8 (CTA): Call-to-action with QR code placeholders
For each page provide:
- pageNumber (1-8)
- type: "cover" | "content" | "cta"
- title: Bold headline
- subtitle: (optional) Supporting text
- keyPoints: Array of 2-4 key points with emojis
- hashtags: 2-3 relevant hashtags
- imagePrompt: Detailed prompt for ${style} style image generation
Output as JSON array.`;
}
/**
* Generate an image prompt for a zine page
*
* @param {Object} options
* @param {number} options.pageNumber - Page number (1-8)
* @param {string} options.zineTopic - Overall zine topic
* @param {Object} options.pageOutline - Page outline from content generation
* @param {string} [options.style='punk-zine'] - Visual style
* @param {string} [options.feedback] - User feedback to incorporate
* @returns {string} Prompt for image generation
*/
export function getImagePrompt({ pageNumber, zineTopic, pageOutline, style = 'punk-zine', feedback = null }) {
const styleDesc = STYLES[style] || STYLES['punk-zine'];
let prompt = `Punk zine page ${pageNumber}/8 for "${zineTopic}".
${pageOutline.imagePrompt || ''}
Style: ${styleDesc}
Include text elements: ${pageOutline.title}
${pageOutline.keyPoints ? pageOutline.keyPoints.join(', ') : ''}
${pageOutline.hashtags ? `Hashtags: ${pageOutline.hashtags.join(' ')}` : ''}`;
if (feedback) {
prompt += `\n\nUser feedback to incorporate: ${feedback}`;
}
return prompt;
}
/**
* Generate an ideation prompt to start zine planning
*
* @param {string} topic - Topic to brainstorm about
* @param {string} [style='punk-zine'] - Visual style preference
* @returns {string} Prompt for ideation brainstorming
*/
export function getIdeationPrompt(topic, style = 'punk-zine') {
return `Let's create an 8-page mycro-zine about "${topic}" in ${style} style.
This is a mini folded zine format - think punk, DIY, memetic, shareable.
Help me brainstorm:
1. What are the key concepts to cover?
2. What's the narrative arc (intro core content call to action)?
3. What visual metaphors or imagery would work well?
4. What hashtags and slogans would resonate?
5. Any source URLs or references to pull from?
Let's iterate on this before generating the actual pages.`;
}
/**
* Page templates for common zine structures
*/
export const PAGE_TEMPLATES = {
cover: {
type: 'cover',
description: 'Bold title, eye-catching visual, sets the tone',
elements: ['title', 'subtitle', 'visual hook', 'issue number (optional)']
},
intro: {
type: 'content',
description: 'What is this about? Hook the reader',
elements: ['question or statement', 'brief explanation', 'why it matters']
},
concept: {
type: 'content',
description: 'Explain a key concept',
elements: ['concept name', 'visual metaphor', 'key points with emojis', 'hashtag']
},
comparison: {
type: 'content',
description: 'Compare/contrast two things',
elements: ['side by side layout', 'pros/cons', 'clear distinction']
},
process: {
type: 'content',
description: 'Step-by-step or flow',
elements: ['numbered steps', 'arrows/flow', 'simple icons']
},
manifesto: {
type: 'content',
description: 'Statement of values/beliefs',
elements: ['bold statements', 'call to action language', 'emotive']
},
resources: {
type: 'content',
description: 'Links, QR codes, further reading',
elements: ['QR codes', 'URLs', 'social handles', 'book/article references']
},
cta: {
type: 'cta',
description: 'Call to action - what should reader do?',
elements: ['action items', 'QR code', 'community links', 'hashtag', 'share prompt']
}
};
/**
* Suggested 8-page structures
*/
export const ZINE_STRUCTURES = {
educational: [
'cover',
'intro',
'concept',
'concept',
'concept',
'process',
'resources',
'cta'
],
manifesto: [
'cover',
'manifesto',
'concept',
'concept',
'comparison',
'manifesto',
'resources',
'cta'
],
howto: [
'cover',
'intro',
'process',
'process',
'process',
'concept',
'resources',
'cta'
]
};
export default {
STYLES,
TONES,
PAGE_TEMPLATES,
ZINE_STRUCTURES,
getContentOutlinePrompt,
getImagePrompt,
getIdeationPrompt
};