diff --git a/.tangled/workflows/deploy-blog.yml b/.tangled/workflows/deploy-blog.yml index 5f9e2a59..7bcc4f43 100644 --- a/.tangled/workflows/deploy-blog.yml +++ b/.tangled/workflows/deploy-blog.yml @@ -21,9 +21,9 @@ steps: - name: generate favicons command: | - ./dolly.out -template appview/pages/templates/fragments/dolly/logo.html -output appview/pages/static/logos/dolly.png -size 180x180 - ./dolly.out -template appview/pages/templates/fragments/dolly/logo.html -output appview/pages/static/logos/dolly.ico -size 48x48 - ./dolly.out -template appview/pages/templates/fragments/dolly/logo.html -output appview/pages/static/logos/dolly.svg -color currentColor -favicon + ./dolly.out -template appview/pages/templates/fragments/dolly -output appview/pages/static/logos/dolly.png -size 180 + ./dolly.out -template appview/pages/templates/fragments/dolly -output appview/pages/static/logos/dolly.ico -size 48 + ./dolly.out -template appview/pages/templates/fragments/dolly -output appview/pages/static/logos/dolly.svg -color currentColor -favicon - name: generate css command: | diff --git a/appview/pages/templates/fragments/dolly/logotype.html b/appview/pages/templates/fragments/dolly/logotype.html index 758d94c9..41d3ac0f 100644 --- a/appview/pages/templates/fragments/dolly/logotype.html +++ b/appview/pages/templates/fragments/dolly/logotype.html @@ -1,5 +1,18 @@ {{ define "fragments/dolly/logotype" }} + {{ if .Favicon }} + + {{ end }} diff --git a/cmd/dolly/main.go b/cmd/dolly/main.go index e1a851f7..aa3b76a7 100644 --- a/cmd/dolly/main.go +++ b/cmd/dolly/main.go @@ -8,6 +8,7 @@ import ( "image" "image/color" "image/png" + "math" "os" "path/filepath" "strconv" @@ -26,13 +27,15 @@ func main() { fillColor string output string templatePath string + kind string favicon bool ) - flag.StringVar(&templatePath, "template", "", "Path to dolly go-html template") - flag.StringVar(&size, "size", "512x512", "Output size in format WIDTHxHEIGHT (e.g., 512x512)") + flag.StringVar(&templatePath, "template", "", "Path to a dolly go-html template file, or a directory of templates") + flag.StringVar(&size, "size", "512", "Output size as WIDTH (height derived from aspect ratio, e.g., 512) or WIDTHxHEIGHT (e.g., 512x512)") flag.StringVar(&fillColor, "color", "#000000", "Fill color in hex format (e.g., #FF5733)") flag.StringVar(&output, "output", "dolly.svg", "Output file path (format detected from extension: .svg, .png, or .ico)") + flag.StringVar(&kind, "kind", "logo", "Asset to generate: logo (dolly only) or logotype (dolly + wordmark)") flag.BoolVar(&favicon, "favicon", false, "Embed a prefers-color-scheme style block so the SVG reacts to dark mode (SVG output only)") flag.Parse() @@ -41,6 +44,11 @@ func main() { os.Exit(1) } + if kind != "logo" && kind != "logotype" { + fmt.Fprintf(os.Stderr, "Invalid kind: %s. Must be logo or logotype\n", kind) + os.Exit(1) + } + width, height, err := parseSize(size) if err != nil { fmt.Fprintf(os.Stderr, "Error parsing size: %v\n", err) @@ -61,9 +69,9 @@ func main() { os.Exit(1) } - tpl, err := os.ReadFile(templatePath) + tpl, err := loadTemplates(templatePath) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to read template from path %s: %v\n", templatePath, err) + fmt.Fprintf(os.Stderr, "Failed to load templates from path %s: %v\n", templatePath, err) os.Exit(1) } @@ -72,12 +80,21 @@ func main() { os.Exit(1) } - svgData, err := dolly(string(tpl), fillColor, favicon) + svgData, err := dolly(tpl, "fragments/dolly/"+kind, fillColor, favicon) if err != nil { fmt.Fprintf(os.Stderr, "Error generating SVG: %v\n", err) os.Exit(1) } + // Derive height from the SVG's aspect ratio when only a width was given + if height == 0 && format != "svg" { + height, err = deriveHeight(svgData, width) + if err != nil { + fmt.Fprintf(os.Stderr, "Error deriving height: %v\n", err) + os.Exit(1) + } + } + // Create output directory if it doesn't exist dir := filepath.Dir(output) if dir != "" && dir != "." { @@ -101,17 +118,30 @@ func main() { os.Exit(1) } - fmt.Printf("Successfully generated %s (%dx%d)\n", output, width, height) + if format == "svg" { + // size is irrelevant for svg output; it scales to its viewBox + fmt.Printf("Successfully generated %s\n", output) + } else { + fmt.Printf("Successfully generated %s (%dx%d)\n", output, width, height) + } } -func dolly(tplString, hexColor string, favicon bool) ([]byte, error) { - tpl, err := template.New("dolly").Parse(tplString) +func loadTemplates(path string) (*template.Template, error) { + info, err := os.Stat(path) if err != nil { return nil, err } + if info.IsDir() { + return template.ParseGlob(filepath.Join(path, "*.html")) + } + + return template.ParseFiles(path) +} + +func dolly(tpl *template.Template, name, hexColor string, favicon bool) ([]byte, error) { var svgData bytes.Buffer - if err := tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", map[string]any{ + if err := tpl.ExecuteTemplate(&svgData, name, map[string]any{ "FillColor": hexColor, "Classes": "", "Favicon": favicon, @@ -138,10 +168,23 @@ func svgToImage(svgData []byte, w, h int) (image.Image, error) { return rgba, nil } +// parseSize parses WIDTH or WIDTHxHEIGHT. A height of 0 means "derive +// from the SVG's aspect ratio". func parseSize(size string) (int, int, error) { + if !strings.Contains(size, "x") { + width, err := strconv.Atoi(size) + if err != nil { + return 0, 0, fmt.Errorf("invalid width: %v", err) + } + if width <= 0 { + return 0, 0, fmt.Errorf("width must be positive") + } + return width, 0, nil + } + parts := strings.Split(size, "x") if len(parts) != 2 { - return 0, 0, fmt.Errorf("invalid size format, use WIDTHxHEIGHT") + return 0, 0, fmt.Errorf("invalid size format, use WIDTH or WIDTHxHEIGHT") } width, err := strconv.Atoi(parts[0]) @@ -161,6 +204,19 @@ func parseSize(size string) (int, int, error) { return width, height, nil } +func deriveHeight(svgData []byte, width int) (int, error) { + icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData)) + if err != nil { + return 0, fmt.Errorf("error parsing SVG: %v", err) + } + + if icon.ViewBox.W <= 0 || icon.ViewBox.H <= 0 { + return 0, fmt.Errorf("SVG has an invalid viewBox (%gx%g)", icon.ViewBox.W, icon.ViewBox.H) + } + + return int(math.Round(float64(width) * icon.ViewBox.H / icon.ViewBox.W)), nil +} + func isValidHexColor(hex string) bool { if len(hex) != 7 || hex[0] != '#' { return false diff --git a/localinfra/scripts/appview-static-files.sh b/localinfra/scripts/appview-static-files.sh index 543ee302..0e6eee4e 100755 --- a/localinfra/scripts/appview-static-files.sh +++ b/localinfra/scripts/appview-static-files.sh @@ -38,7 +38,7 @@ git clone --depth=1 "$ACTOR_TYPEAHEAD_REPO" "$TMP/actor-typeahead" cp -f "$TMP/actor-typeahead/actor-typeahead.js" "$OUT/" (cd "$REPO_ROOT" && go build -o "$TMP/dolly" ./cmd/dolly) -TEMPLATE="$REPO_ROOT/appview/pages/templates/fragments/dolly/logo.html" -"$TMP/dolly" -template "$TEMPLATE" -output "$OUT/logos/dolly.png" -size 180x180 -"$TMP/dolly" -template "$TEMPLATE" -output "$OUT/logos/dolly.ico" -size 48x48 +TEMPLATE="$REPO_ROOT/appview/pages/templates/fragments/dolly" +"$TMP/dolly" -template "$TEMPLATE" -output "$OUT/logos/dolly.png" -size 180 +"$TMP/dolly" -template "$TEMPLATE" -output "$OUT/logos/dolly.ico" -size 48 "$TMP/dolly" -template "$TEMPLATE" -output "$OUT/logos/dolly.svg" -color currentColor -favicon diff --git a/nix/pkgs/appview-static-files.nix b/nix/pkgs/appview-static-files.nix index 50360611..a1b3f795 100644 --- a/nix/pkgs/appview-static-files.nix +++ b/nix/pkgs/appview-static-files.nix @@ -30,8 +30,8 @@ runCommandLocal "appview-static-files" { cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono*.woff2 fonts/ cp -f ${actor-typeahead-src}/actor-typeahead.js . - ${dolly}/bin/dolly -output logos/dolly.png -size 180x180 - ${dolly}/bin/dolly -output logos/dolly.ico -size 48x48 + ${dolly}/bin/dolly -output logos/dolly.png -size 180 + ${dolly}/bin/dolly -output logos/dolly.ico -size 48 ${dolly}/bin/dolly -output logos/dolly.svg -color currentColor -favicon # tailwindcss -c $src/tailwind.config.js -i $src/input.css -o tw.css won't work # for whatever reason (produces broken css), so we are doing this instead diff --git a/nix/pkgs/docs.nix b/nix/pkgs/docs.nix index c5084be8..90d72c0f 100644 --- a/nix/pkgs/docs.nix +++ b/nix/pkgs/docs.nix @@ -54,8 +54,8 @@ runCommandLocal "docs" {} '' cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono*.woff2 $out/static/fonts/ # favicons - ${dolly}/bin/dolly -output $out/static/logos/dolly.png -size 180x180 - ${dolly}/bin/dolly -output $out/static/logos/dolly.ico -size 48x48 + ${dolly}/bin/dolly -output $out/static/logos/dolly.png -size 180 + ${dolly}/bin/dolly -output $out/static/logos/dolly.ico -size 48 ${dolly}/bin/dolly -output $out/static/logos/dolly.svg -color currentColor -favicon # styles diff --git a/nix/pkgs/dolly.nix b/nix/pkgs/dolly.nix index e9250185..e4df4595 100644 --- a/nix/pkgs/dolly.nix +++ b/nix/pkgs/dolly.nix @@ -11,6 +11,7 @@ ../../ico ../../cmd/dolly/main.go ../../appview/pages/templates/fragments/dolly/logo.html + ../../appview/pages/templates/fragments/dolly/logotype.html ]; }; dolly-unwrapped = buildGoApplication { @@ -23,6 +24,6 @@ in writeShellScriptBin "dolly" '' exec ${dolly-unwrapped}/bin/dolly \ - -template ${src}/appview/pages/templates/fragments/dolly/logo.html \ + -template ${src}/appview/pages/templates/fragments/dolly \ "$@" ''