1 Commits
Author SHA1 Message Date
Charles Magahern e075834f98 "Street Geek" fashion article
Build PDF / make pdf (pull_request) Successful in 4m48s
2026-07-05 19:57:38 -07:00
15 changed files with 586 additions and 766 deletions
+3 -4
View File
@@ -27,8 +27,6 @@ serve: deps
PDF := $(OUT_DIR)/output.pdf
PDF_2UP := $(OUT_DIR)/output-2up.pdf
PDF_IMAGE_DPI ?= 144
PDF_JPEG_QUALITY ?= 75
.PHONY: deps
deps:
@@ -37,12 +35,13 @@ deps:
.PHONY: pdf
pdf: build deps
@echo "Generating PDF by rendering each page and merging..."
@$(GO) run ./cmd/pdfbook --pages $(OUT_DIR) --order pages.yaml --out $(PDF) --image-dpi $(PDF_IMAGE_DPI) --jpeg-quality $(PDF_JPEG_QUALITY)
@$(GO) run ./cmd/pdfbook --pages $(OUT_DIR) --order pages.yaml --out $(PDF)
.PHONY: pdf-2up
pdf-2up: build deps
@echo "Generating 2-up PDF with headless Chrome..."
@$(GO) run ./cmd/pdf --in $(OUT_DIR)/print_2up.html --out $(PDF_2UP) --w 11 --h 8.5 --image-dpi $(PDF_IMAGE_DPI) --jpeg-quality $(PDF_JPEG_QUALITY)
@$(GO) run ./cmd/pdf --in $(OUT_DIR)/print_2up.html --out $(PDF_2UP) --w 11 --h 8.5
clean:
rm -rf $(OUT_DIR) index.html
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

-31
View File
@@ -10,7 +10,6 @@ import (
"path/filepath"
"smartbar/internal/config"
"smartbar/internal/pdfassets"
"github.com/chromedp/cdproto/emulation"
"github.com/chromedp/cdproto/page"
@@ -43,15 +42,11 @@ func main() {
output string
width float64
height float64
imageDPI int
jpegQuality int
)
flag.StringVar(&input, "in", "", "input HTML file path (required)")
flag.StringVar(&output, "out", "", "output PDF path (required)")
flag.Float64Var(&width, "w", config.PageWidthIn, "page width in inches")
flag.Float64Var(&height, "h", config.PageHeightIn, "page height in inches")
flag.IntVar(&imageDPI, "image-dpi", 144, "downsample image assets to this PDF pixel density; 0 disables")
flag.IntVar(&jpegQuality, "jpeg-quality", 75, "JPEG quality for PDF image assets")
flag.Parse()
if input == "" || output == "" {
@@ -59,32 +54,6 @@ func main() {
}
absInput, _ := filepath.Abs(input)
inputRoot := filepath.Dir(absInput)
inputRel := filepath.Base(absInput)
imageOpts := pdfassets.OptionsForPage(width, height, imageDPI, jpegQuality)
prepared, err := pdfassets.PrepareTree(inputRoot, imageOpts)
if err != nil {
log.Fatal(err)
}
defer func() {
if err := prepared.Cleanup(); err != nil {
log.Printf("warning: cleanup optimized PDF assets: %v", err)
}
}()
if prepared.Stats.ImageFiles > 0 {
fmt.Printf(
"Optimized PDF images: %d/%d files, %s -> %s (max %dx%d, JPEG quality %d)\n",
prepared.Stats.OptimizedImages,
prepared.Stats.ImageFiles,
pdfassets.FormatBytes(prepared.Stats.OriginalBytes),
pdfassets.FormatBytes(prepared.Stats.OutputBytes),
imageOpts.MaxWidth,
imageOpts.MaxHeight,
imageOpts.JPEGQuality,
)
}
absInput = filepath.Join(prepared.Root, inputRel)
url := "file://" + absInput
execPath, err := findChromeExec()
+2 -27
View File
@@ -11,7 +11,6 @@ import (
"strings"
"smartbar/internal/config"
"smartbar/internal/pdfassets"
"github.com/chromedp/cdproto/emulation"
"github.com/chromedp/cdproto/page"
@@ -75,16 +74,12 @@ func main() {
outPDF string
width float64
height float64
imageDPI int
jpegQuality int
)
flag.StringVar(&pagesDir, "pages", "_dist", "path to compiled pages directory (required)")
flag.StringVar(&orderPath, "order", "pages.yaml", "path to YAML file listing page order (required)")
flag.StringVar(&outPDF, "out", "_dist/output.pdf", "output PDF path (required)")
flag.Float64Var(&width, "w", config.PageWidthIn, "page width in inches")
flag.Float64Var(&height, "h", config.PageHeightIn, "page height in inches")
flag.IntVar(&imageDPI, "image-dpi", 144, "downsample image assets to this PDF pixel density; 0 disables")
flag.IntVar(&jpegQuality, "jpeg-quality", 75, "JPEG quality for PDF image assets")
flag.Parse()
if pagesDir == "" || orderPath == "" || outPDF == "" {
@@ -98,28 +93,6 @@ func main() {
ordered, err := parseYAMLListOfStrings(string(data))
must(err)
imageOpts := pdfassets.OptionsForPage(width, height, imageDPI, jpegQuality)
prepared, err := pdfassets.PrepareTree(pagesDir, imageOpts)
must(err)
defer func() {
if err := prepared.Cleanup(); err != nil {
log.Printf("warning: cleanup optimized PDF assets: %v", err)
}
}()
pagesDir = prepared.Root
if prepared.Stats.ImageFiles > 0 {
fmt.Printf(
"Optimized PDF images: %d/%d files, %s -> %s (max %dx%d, JPEG quality %d)\n",
prepared.Stats.OptimizedImages,
prepared.Stats.ImageFiles,
pdfassets.FormatBytes(prepared.Stats.OriginalBytes),
pdfassets.FormatBytes(prepared.Stats.OutputBytes),
imageOpts.MaxWidth,
imageOpts.MaxHeight,
imageOpts.JPEGQuality,
)
}
// Resolve Chrome
chromeExec, err := findChromeExec()
must(err)
@@ -181,3 +154,5 @@ func main() {
must(pdfapi.MergeCreateFile(partPDFs, outPDF, false, nil))
fmt.Printf("Wrote %s (%d pages)\n", outPDF, len(partPDFs))
}
+1 -1
View File
@@ -9,7 +9,6 @@ require (
github.com/chromedp/chromedp v0.9.5
github.com/fsnotify/fsnotify v1.9.0
github.com/pdfcpu/pdfcpu v0.11.0
golang.org/x/image v0.27.0
)
require (
@@ -26,6 +25,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/image v0.27.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
-470
View File
@@ -1,470 +0,0 @@
package pdfassets
import (
"bytes"
"encoding/binary"
"fmt"
"image"
stddraw "image/draw"
"image/jpeg"
"image/png"
"io"
"io/fs"
"math"
"os"
"path/filepath"
"strings"
xdraw "golang.org/x/image/draw"
)
type Options struct {
MaxWidth int
MaxHeight int
JPEGQuality int
}
type Stats struct {
ImageFiles int
OptimizedImages int
OriginalBytes int64
OutputBytes int64
}
type PreparedTree struct {
Root string
Stats Stats
Cleanup func() error
}
func OptionsForPage(widthIn, heightIn float64, imageDPI, jpegQuality int) Options {
if imageDPI <= 0 {
return Options{JPEGQuality: clampJPEGQuality(jpegQuality)}
}
return Options{
MaxWidth: max(1, int(math.Ceil(widthIn*float64(imageDPI)))),
MaxHeight: max(1, int(math.Ceil(heightIn*float64(imageDPI)))),
JPEGQuality: clampJPEGQuality(jpegQuality),
}
}
func (o Options) Enabled() bool {
return o.MaxWidth > 0 && o.MaxHeight > 0
}
func PrepareTree(srcRoot string, opts Options) (PreparedTree, error) {
if !opts.Enabled() {
return PreparedTree{
Root: srcRoot,
Cleanup: func() error { return nil },
}, nil
}
absRoot, err := filepath.Abs(srcRoot)
if err != nil {
return PreparedTree{}, err
}
tmpRoot, err := os.MkdirTemp("", "smartbar_pdf_assets_*")
if err != nil {
return PreparedTree{}, err
}
prepared := PreparedTree{
Root: tmpRoot,
Cleanup: func() error { return os.RemoveAll(tmpRoot) },
}
err = filepath.WalkDir(absRoot, func(path string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
rel, err := filepath.Rel(absRoot, path)
if err != nil {
return err
}
if rel == "." {
return nil
}
dst := filepath.Join(tmpRoot, rel)
if d.IsDir() {
return os.MkdirAll(dst, 0o755)
}
info, err := d.Info()
if err != nil {
return err
}
if info.Mode()&fs.ModeSymlink != 0 {
return copySymlink(path, dst)
}
if !info.Mode().IsRegular() {
return nil
}
if strings.EqualFold(filepath.Ext(path), ".pdf") {
return nil
}
if isSupportedImage(path) {
stats, err := optimizeImageFile(path, dst, info.Mode().Perm(), opts)
if err == nil {
prepared.Stats.ImageFiles++
prepared.Stats.OriginalBytes += stats.originalBytes
prepared.Stats.OutputBytes += stats.outputBytes
if stats.optimized {
prepared.Stats.OptimizedImages++
}
return nil
}
}
return copyFile(path, dst, info.Mode().Perm())
})
if err != nil {
_ = prepared.Cleanup()
return PreparedTree{}, err
}
return prepared, nil
}
type imageFileStats struct {
originalBytes int64
outputBytes int64
optimized bool
}
func optimizeImageFile(src, dst string, mode fs.FileMode, opts Options) (imageFileStats, error) {
data, err := os.ReadFile(src)
if err != nil {
return imageFileStats{}, err
}
optimized, changed, err := optimizeImage(data, filepath.Ext(src), opts)
if err != nil {
return imageFileStats{}, err
}
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return imageFileStats{}, err
}
output := optimized
if !changed {
output = data
}
if err := os.WriteFile(dst, output, mode); err != nil {
return imageFileStats{}, err
}
return imageFileStats{
originalBytes: int64(len(data)),
outputBytes: int64(len(output)),
optimized: changed,
}, nil
}
func optimizeImage(data []byte, ext string, opts Options) ([]byte, bool, error) {
img, format, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil, false, err
}
orientation := 1
if format == "jpeg" || strings.EqualFold(ext, ".jpg") || strings.EqualFold(ext, ".jpeg") {
orientation = jpegOrientation(data)
img = applyOrientation(img, orientation)
}
srcBounds := img.Bounds()
dstW, dstH := fitWithin(srcBounds.Dx(), srcBounds.Dy(), opts.MaxWidth, opts.MaxHeight)
resized := dstW != srcBounds.Dx() || dstH != srcBounds.Dy()
if resized {
dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH))
xdraw.CatmullRom.Scale(dst, dst.Bounds(), img, srcBounds, stddraw.Src, nil)
img = dst
}
var buf bytes.Buffer
if imageHasTransparency(img) {
encoder := png.Encoder{CompressionLevel: png.BestCompression}
err = encoder.Encode(&buf, img)
} else {
err = jpeg.Encode(&buf, img, &jpeg.Options{Quality: clampJPEGQuality(opts.JPEGQuality)})
}
if err != nil {
return nil, false, err
}
changed := resized || orientation != 1 || buf.Len() < len(data)
if !changed {
return data, false, nil
}
return buf.Bytes(), true, nil
}
func fitWithin(width, height, maxWidth, maxHeight int) (int, int) {
if width <= 0 || height <= 0 || maxWidth <= 0 || maxHeight <= 0 {
return width, height
}
if width <= maxWidth && height <= maxHeight {
return width, height
}
scale := math.Min(float64(maxWidth)/float64(width), float64(maxHeight)/float64(height))
return max(1, int(math.Round(float64(width)*scale))), max(1, int(math.Round(float64(height)*scale)))
}
func imageHasTransparency(img image.Image) bool {
switch m := img.(type) {
case *image.YCbCr:
return false
case *image.Gray, *image.Gray16, *image.CMYK:
return false
case *image.NRGBA:
for i := 3; i < len(m.Pix); i += 4 {
a := m.Pix[i]
if a != 0xff {
return true
}
}
return false
case *image.RGBA:
for i := 3; i < len(m.Pix); i += 4 {
a := m.Pix[i]
if a != 0xff {
return true
}
}
return false
case *image.Alpha, *image.Alpha16:
return true
}
b := img.Bounds()
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
_, _, _, a := img.At(x, y).RGBA()
if a != 0xffff {
return true
}
}
}
return false
}
func applyOrientation(img image.Image, orientation int) image.Image {
if orientation < 2 || orientation > 8 {
return img
}
b := img.Bounds()
w, h := b.Dx(), b.Dy()
dstW, dstH := w, h
if orientation >= 5 {
dstW, dstH = h, w
}
dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH))
for y := 0; y < dstH; y++ {
for x := 0; x < dstW; x++ {
sx, sy := orientedSourcePoint(x, y, w, h, orientation)
dst.Set(x, y, img.At(b.Min.X+sx, b.Min.Y+sy))
}
}
return dst
}
func orientedSourcePoint(x, y, width, height, orientation int) (int, int) {
switch orientation {
case 2:
return width - 1 - x, y
case 3:
return width - 1 - x, height - 1 - y
case 4:
return x, height - 1 - y
case 5:
return y, x
case 6:
return y, height - 1 - x
case 7:
return width - 1 - y, height - 1 - x
case 8:
return width - 1 - y, x
default:
return x, y
}
}
func jpegOrientation(data []byte) int {
if len(data) < 4 || data[0] != 0xff || data[1] != 0xd8 {
return 1
}
for i := 2; i+4 <= len(data); {
if data[i] != 0xff {
i++
continue
}
for i < len(data) && data[i] == 0xff {
i++
}
if i >= len(data) {
return 1
}
marker := data[i]
i++
if marker == 0xd9 || marker == 0xda {
return 1
}
if marker >= 0xd0 && marker <= 0xd7 {
continue
}
if i+2 > len(data) {
return 1
}
segmentLen := int(binary.BigEndian.Uint16(data[i : i+2]))
i += 2
if segmentLen < 2 || i+segmentLen-2 > len(data) {
return 1
}
payload := data[i : i+segmentLen-2]
if marker == 0xe1 && bytes.HasPrefix(payload, []byte("Exif\x00\x00")) {
return tiffOrientation(payload[6:])
}
i += segmentLen - 2
}
return 1
}
func tiffOrientation(data []byte) int {
if len(data) < 8 {
return 1
}
var order binary.ByteOrder
switch string(data[:2]) {
case "II":
order = binary.LittleEndian
case "MM":
order = binary.BigEndian
default:
return 1
}
if order.Uint16(data[2:4]) != 42 {
return 1
}
ifdOffset := int(order.Uint32(data[4:8]))
if ifdOffset < 0 || ifdOffset+2 > len(data) {
return 1
}
entryCount := int(order.Uint16(data[ifdOffset : ifdOffset+2]))
pos := ifdOffset + 2
for i := 0; i < entryCount; i++ {
if pos+12 > len(data) {
return 1
}
tag := order.Uint16(data[pos : pos+2])
fieldType := order.Uint16(data[pos+2 : pos+4])
count := order.Uint32(data[pos+4 : pos+8])
if tag == 0x0112 && fieldType == 3 && count == 1 {
value := int(order.Uint16(data[pos+8 : pos+10]))
if value >= 1 && value <= 8 {
return value
}
return 1
}
pos += 12
}
return 1
}
func isSupportedImage(path string) bool {
switch strings.ToLower(filepath.Ext(path)) {
case ".jpg", ".jpeg", ".png":
return true
default:
return false
}
}
func copyFile(src, dst string, mode fs.FileMode) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
if err != nil {
return err
}
if _, err := io.Copy(out, in); err != nil {
_ = out.Close()
return err
}
return out.Close()
}
func copySymlink(src, dst string) error {
target, err := os.Readlink(src)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
return os.Symlink(target, dst)
}
func clampJPEGQuality(q int) int {
if q <= 0 {
return 82
}
if q < 1 {
return 1
}
if q > 100 {
return 100
}
return q
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func FormatBytes(n int64) string {
const unit = 1024
if n < unit {
return fmt.Sprintf("%d B", n)
}
div, exp := int64(unit), 0
for n >= unit*div && exp < 4 {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), "KMGTPE"[exp])
}
-100
View File
@@ -1,100 +0,0 @@
package pdfassets
import (
"bytes"
"image"
"image/color"
"image/jpeg"
"os"
"path/filepath"
"testing"
)
func TestFitWithin(t *testing.T) {
w, h := fitWithin(400, 200, 100, 100)
if w != 100 || h != 50 {
t.Fatalf("fitWithin landscape = %dx%d, want 100x50", w, h)
}
w, h = fitWithin(200, 400, 100, 100)
if w != 50 || h != 100 {
t.Fatalf("fitWithin portrait = %dx%d, want 50x100", w, h)
}
}
func TestApplyOrientationRightTop(t *testing.T) {
img := image.NewRGBA(image.Rect(0, 0, 2, 3))
for y := 0; y < 3; y++ {
for x := 0; x < 2; x++ {
img.Set(x, y, color.RGBA{R: uint8(x), G: uint8(y), A: 0xff})
}
}
got := applyOrientation(img, 6)
if got.Bounds().Dx() != 3 || got.Bounds().Dy() != 2 {
t.Fatalf("oriented bounds = %v, want 3x2", got.Bounds())
}
assertRGBA(t, got.At(0, 0), color.RGBA{R: 0, G: 2, A: 0xff})
assertRGBA(t, got.At(2, 1), color.RGBA{R: 1, G: 0, A: 0xff})
}
func TestPrepareTreeOptimizesImagesAndSkipsPDFs(t *testing.T) {
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "assets", "img"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "index.html"), []byte(`<img src="assets/img/photo.jpg">`), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "old.pdf"), []byte("old pdf"), 0o644); err != nil {
t.Fatal(err)
}
var src bytes.Buffer
img := image.NewRGBA(image.Rect(0, 0, 400, 200))
for y := 0; y < 200; y++ {
for x := 0; x < 400; x++ {
img.Set(x, y, color.RGBA{R: uint8(x), G: uint8(y), B: 120, A: 0xff})
}
}
if err := jpeg.Encode(&src, img, &jpeg.Options{Quality: 95}); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "assets", "img", "photo.jpg"), src.Bytes(), 0o644); err != nil {
t.Fatal(err)
}
prepared, err := PrepareTree(root, Options{MaxWidth: 100, MaxHeight: 100, JPEGQuality: 70})
if err != nil {
t.Fatal(err)
}
defer prepared.Cleanup()
if prepared.Stats.ImageFiles != 1 || prepared.Stats.OptimizedImages != 1 {
t.Fatalf("stats = %+v, want one optimized image", prepared.Stats)
}
if _, err := os.Stat(filepath.Join(prepared.Root, "old.pdf")); !os.IsNotExist(err) {
t.Fatalf("old PDF copied into prepared tree; err=%v", err)
}
out, err := os.Open(filepath.Join(prepared.Root, "assets", "img", "photo.jpg"))
if err != nil {
t.Fatal(err)
}
defer out.Close()
cfg, err := jpeg.DecodeConfig(out)
if err != nil {
t.Fatal(err)
}
if cfg.Width > 100 || cfg.Height > 100 {
t.Fatalf("optimized dimensions = %dx%d, want within 100x100", cfg.Width, cfg.Height)
}
}
func assertRGBA(t *testing.T, got color.Color, want color.RGBA) {
t.Helper()
r, g, b, a := got.RGBA()
if uint8(r>>8) != want.R || uint8(g>>8) != want.G || uint8(b>>8) != want.B || uint8(a>>8) != want.A {
t.Fatalf("color = rgba(%d,%d,%d,%d), want %+v", uint8(r>>8), uint8(g>>8), uint8(b>>8), uint8(a>>8), want)
}
}
+3
View File
@@ -13,6 +13,9 @@
- gtkapplang-3.html
- gtkapplang-4.html
- broken-screen.html
- fashion.html
- streetgeek-1.html
- streetgeek-2.html
- windows-shirt.html
- free-dirt.html
- mr-nickel.html
+9
View File
@@ -0,0 +1,9 @@
<style>
#fashion {
background-image: url("assets/img/fashion.jpg");
background-size: contain;
}
</style>
<div id="fashion" class="page-base">
</div>
+194
View File
@@ -0,0 +1,194 @@
<style>
@font-face {
font-family: "Aerosoldier";
src: url("assets/font/aerosoldier.otf");
}
@font-face {
font-family: "COMPUTER Robot";
src: url("assets/font/computer-robot.ttf");
}
span.computer {
font-family: "COMPUTER Robot", monospace;
}
#streetgeek1-page {
display: flex;
flex-direction: column;
padding: 0.4in 0.42in;
background-color: white;
color: black;
font: 8.6pt/1.42 'Times New Roman', serif;
}
#sg1-kicker {
font-family: Helvetica, Arial, sans-serif;
font-size: 7pt;
font-weight: bold;
letter-spacing: 0.22em;
text-transform: uppercase;
color: rgb(209, 1, 209);
margin-bottom: 3pt;
}
#streetgeek1-page h1 {
font-family: Aerosoldier, Helvetica, sans-serif;
font-size: 38pt;
line-height: 0.85;
color: black;
margin: 0;
}
#sg1-deck {
font-family: Georgia, 'Times New Roman', serif;
font-style: italic;
font-size: 10pt;
color: #222;
margin: 5pt 0 3pt 0;
}
#sg1-byline {
font-size: 8pt;
font-style: italic;
margin: 0;
}
#sg1-rule {
border: none;
border-top: 1.4pt solid black;
margin: 6pt 0 9pt 0;
}
#sg1-body {
flex: 1;
columns: 2;
column-gap: 0.28in;
}
#sg1-body p {
color: black;
text-indent: 2em;
text-align: justify;
text-justify: inter-word;
hyphens: auto;
overflow-wrap: break-word;
margin: 0 0 7pt 0;
}
.drop-cap::first-letter {
float: left;
font-size: 4em;
line-height: 0.8;
padding-right: 8px;
padding-top: 0px;
font-weight: bold;
font-family: Georgia, serif;
}
.drop-cap {
text-indent: 0 !important;
}
#sg1-pullquote {
break-inside: avoid;
-webkit-column-break-inside: avoid;
font-family: Georgia, 'Times New Roman', serif;
font-style: italic;
font-size: 13.5pt;
line-height: 1.25;
color: #111;
text-indent: 0;
border-left: 3pt solid rgb(209, 1, 209);
padding: 2pt 0 2pt 10pt;
margin: 4pt 0 9pt 0;
}
#sg1-footer {
display: flex;
justify-content: space-between;
align-items: baseline;
border-top: 0.75pt solid black;
margin-top: 6pt;
padding-top: 4pt;
font-family: Helvetica, Arial, sans-serif;
font-size: 6.5pt;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #333;
}
</style>
<div id="streetgeek1-page" class="page-base">
<div id="sg1-header">
<h1>Street Geek</h1>
<p id="sg1-deck">Notes from the front lines of hacker haute couture.</p>
<p id="sg1-byline">By <strong>Bram Noidz</strong></p>
</div>
<hr id="sg1-rule" />
<div id="sg1-body">
<p class="drop-cap">
My day in meatspace wasn't going well. I woke up in the late morning and acquired the day's
outfit using my usual clothing drawer LIFO mechanism. This week's permutation of clothing in the
drawer left me with a depressingly common combination: black t-shirt with black jeans. I thought
for a brief second that I should break out of the LIFO ordering and make a daring attempt at a
more arbitrarily stylish outfit. Brief panic attack. Ultimately I decided against it. <i>The
purpose of a system is what it does.</i> If I decide for even one day to ignore my LIFO
clothing system, then it really is no system at all.
</p>
<p>
I hobbled downstairs in my algorithmically chosen outfit and made a cup of coffee. Double
espresso with a bit of hot water mixed in. While reading something on my computer, I
accidentally spilled about half of the coffee onto my shirt. Luckily for me, the stains were
practically invisible on the black t-shirt, and very little ended up on my pants. The only
evidence that I had spilled something on myself that morning was the faint coffee smell that
lingered after I mopped it up. But who doesn't love the smell of coffee? I actually wouldn't
mind a cologne with the scent of the glorious roasted bean imbued into it. Similar to the
clickety-clack sound of the mechanical keyboard, the coffee cologne might have the Pavlovian
effect of stimulating the electronic productivity of the people around you. Free idea for any of
our young entrepreneurial readers out there.
</p>
<p>
You might be wondering why I spent more time than usual at this time of the morning thinking
about what I'm wearing. Today, I am meeting my friend <span class="computer">ByteLord</span> for lunch at the vegan
restaurant <i>Cha-Ya</i> on Valencia. <span class="computer">ByteLord</span> is a one-of-a-kind genius level programmer
that I met on an IRC channel one evening. This guy slings code so neat and clean, he puts Linus
Torvalds and that guy who made the <i>McMaster-Carr</i> catalog website to shame. I had never
met <span class="computer">ByteLord</span> A.F.K. before. Therefore, needless to say, I felt intimidated and eager to
please.
</p>
<div id="sg1-pullquote">&ldquo;The purpose of a system is what it does.&rdquo;</div>
<p>
I got to <i>Cha-Ya</i> a little bit early and ordered a green tea while waiting for
<span class="computer">ByteLord</span> to arrive. I spilled a little bit of this on my shirt as well. Unfortunately
now, mixed with the coffee stain from earlier, the combination gave off a toilet smell that was
quite unpleasant. I started praying that <span class="computer">ByteLord</span> had a cold and wouldn't be able to
smell anything. Luckily the stains were still mostly invisible on my black t-shirt.
</p>
<p>
When <span class="computer">ByteLord</span> walked in the front door, I almost mistook him for one of those cool
hip-hopper guys. He was decked out from head to toe and looked <i>on fleek</i>.
</p>
</div>
<!--
<div id="sg1-footer">
<span>Smart Bar</span>
<span>Continued on page 02 &rarr;</span>
<span>01</span>
</div>
-->
</div>
+241
View File
@@ -0,0 +1,241 @@
<style>
@font-face {
font-family: "Aerosoldier";
src: url("assets/font/aerosoldier.otf");
}
@font-face {
font-family: "COMPUTER Robot";
src: url("assets/font/computer-robot.ttf");
}
@font-face {
font-family: "TS Block Bold";
src: url("assets/font/ts-block-bold.ttf");
}
span.computer {
font-family: "COMPUTER Robot", monospace;
}
span.cyberwheel {
font-family: "TS Block Bold";
}
#streetgeek2-page {
display: flex;
flex-direction: column;
padding: 0.3in 0.4in;
background-color: white;
color: black;
font: 8.6pt/1.42 'Times New Roman', serif;
}
#sg2-topbar {
display: flex;
justify-content: space-between;
align-items: baseline;
font-family: Helvetica, Arial, sans-serif;
font-size: 6.5pt;
font-weight: bold;
letter-spacing: 0.16em;
text-transform: uppercase;
color: #333;
border-bottom: 0.75pt solid black;
padding-bottom: 4pt;
margin-bottom: 8pt;
}
#sg2-topbar span:first-child {
color: rgb(209, 1, 209);
}
#sg2-headline {
font-family: Aerosoldier, Helvetica, sans-serif;
font-size: 24pt;
line-height: 0.9;
margin: 0 0 3pt 0;
}
#sg2-subhead {
font-family: Georgia, 'Times New Roman', serif;
font-style: italic;
font-size: 9.5pt;
color: #222;
margin: 0 0 10pt 0;
}
#sg2-gallery {
display: flex;
justify-content: center;
align-items: flex-start;
gap: 0.2in;
margin: 3pt 0 8pt 0;
}
.sg2-frame {
background: white;
padding: 6px 6px 5px 6px;
border: 1px solid #ddd;
box-shadow: 0 3px 10px rgba(0,0,0,0.28);
break-inside: avoid;
-webkit-column-break-inside: avoid;
}
.sg2-frame.left, .sg2-frame.right {
width: 1.95in;
}
.sg2-frame.left { transform: rotate(-2.4deg); }
.sg2-frame.right { transform: rotate(2.2deg); }
.sg2-frame.solo {
float: right;
width: 46%;
margin: 2pt 0 4pt 10px;
transform: rotate(-1.6deg);
}
.sg2-frame img {
display: block;
width: 100%;
height: auto;
}
.sg2-caption {
font-family: Helvetica, Arial, sans-serif;
font-size: 6.5pt;
letter-spacing: 0.1em;
text-transform: uppercase;
text-align: center;
color: #333;
margin-top: 4px;
}
#sg2-text {
flex: 1;
columns: 2;
column-gap: 0.26in;
}
#sg2-text p {
text-indent: 2em;
text-align: justify;
text-justify: inter-word;
hyphens: auto;
overflow-wrap: break-word;
margin: 0 0 4pt 0;
}
#sg2-text p:first-child {
text-indent: 0;
}
#sg2-text p:first-child::first-letter {
float: left;
font-size: 3em;
line-height: 0.78;
padding-right: 6px;
font-weight: bold;
font-family: Georgia, serif;
}
#sg2-footer {
display: flex;
justify-content: space-between;
align-items: baseline;
border-top: 0.75pt solid black;
margin-top: 6pt;
padding-top: 4pt;
font-family: Helvetica, Arial, sans-serif;
font-size: 6.5pt;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #333;
}
</style>
<div id="streetgeek2-page" class="page-base">
<!--
<div id="sg2-topbar">
<span>Street Geek</span>
<span>Field Report</span>
<span>Page 02</span>
</div>
-->
<!--
<div id="sg2-header">
<h2 id="sg2-headline">The Look</h2>
<p id="sg2-subhead">ByteLord, spotted in the wild at Cha-Ya on Valencia.</p>
</div>
-->
<div id="sg2-gallery">
<div class="sg2-frame left">
<img src="assets/img/streetgeek-java.png" alt="ByteLord wearing a Java t-shirt under a gray plaid overshirt" />
<div class="sg2-caption">&quot;Java&quot;</div>
</div>
<div class="sg2-frame right">
<img src="assets/img/streetgeek-evangelion.png" alt="ByteLord wearing an Evangelion t-shirt under a black overshirt" />
<div class="sg2-caption">&quot;Evangelion&quot;</div>
</div>
</div>
<div id="sg2-text">
<p>
A typically chilly San Francisco summer afternoon, he was dressed for warmth. A beanie with an
off-white color, reminiscent of the Windows XP start menu background. A comfy gray overshirt
with a clean geometric pattern. Some kind of smart watch whose battery was obviously dead. Most
tellingly, a <i>Java</i> t-shirt underneath the overshirt which, in the context of the rest of
the outfit, gave the programming language an undeserved level of coolness.
</p>
<div class="sg2-frame solo">
<img src="assets/img/streetgeek-dogtags.jpeg" alt="ByteLord's necklace: a Mac OS X Panther dog tag on a ball chain" />
</div>
<p>
Besides the comfortable textiles, I was impressed with <span class="computer">ByteLord</span>'s choice of accessories.
A wallet chain, which I hadn't seen anyone wear since the mid 2000's, added a metallic balance
to the lower half of his appearance. His neck was adorned with a necklace that didn't appear to
serve any functional purpose, but matched the wallet chain quite nicely. Finally, he was wearing
rather large, television-sized eyeglass frames. Frankly, I had never given much thought to
accessories in an outfit, unless they have some kind of microchip in them. But <span class="computer">ByteLord</span>
had arranged them in such a way to make the outfit look effortless, yet stylish.
</p>
<p>
We ordered <i>zaru soba</i> with <i>vegetable tempura</i> and spoke about various topics from
cyberspace. After we finished talking about the guy that supposedly defaced fellow editor P. M.
Cho's website, we started talking about fashion. I was surprised to hear that <span class="computer">ByteLord</span>
was quite into fashion on a deeply philosophical level. He explained to me that what you wear in
meatspace is like the <i>graphical shell</i> for a person. A hacker should present himself to
others in the same elegant way that a computer program presents itself to users. You wouldn't
want to walk away from an interaction with someone having the same feeling as using the
<i>git</i> command line interface.
</p>
<p> I paid for the <i>zaru soba</i> and <span class="computer">ByteLord</span> sent me some <span class="cyberwheel">SMARTCOIN</span> for his
portion. We parted ways, each walking opposite directions on Valencia street. After our meeting,
I couldn't help but notice and observe all of the outfits people were wearing that day in the
Mission. Thousands of distinct <i>graphical shells</i> all populating the virtual desktop that
is our city. I smiled, and started thinking about a new ordering system for my wardrobe.
<span class="endmark"></span>
</p>
</div>
<!--
<div id="sg2-footer">
<span>Smart Bar</span>
<span>&#9632;</span>
<span>02</span>
</div>
-->
</div>