From 47988812ffce506c75764137bb6774d289dd003c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:12:17 +0800 Subject: [PATCH 1/3] brand(#18): add ByteFolk organization avatar assets --- .github/workflows/validate.yml | 3 + brand/bytefolk/README.md | 46 +++++++++ brand/bytefolk/avatar-1024.png | Bin 0 -> 8163 bytes brand/bytefolk/lockup.svg | 20 ++++ brand/bytefolk/render-avatar.sh | 49 +++++++++ brand/bytefolk/symbol-reversed.svg | 10 ++ brand/bytefolk/symbol.svg | 10 ++ scripts/validate-brand-assets.rb | 158 +++++++++++++++++++++++++++++ 8 files changed, 296 insertions(+) create mode 100644 brand/bytefolk/README.md create mode 100644 brand/bytefolk/avatar-1024.png create mode 100644 brand/bytefolk/lockup.svg create mode 100755 brand/bytefolk/render-avatar.sh create mode 100644 brand/bytefolk/symbol-reversed.svg create mode 100644 brand/bytefolk/symbol.svg create mode 100644 scripts/validate-brand-assets.rb diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index fd3758f..6056212 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -30,6 +30,9 @@ jobs: - name: Validate issue forms and Markdown links run: ruby scripts/validate-governance.rb + - name: Validate ByteFolk brand assets + run: ruby scripts/validate-brand-assets.rb + - name: Check whitespace env: BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} diff --git a/brand/bytefolk/README.md b/brand/bytefolk/README.md new file mode 100644 index 0000000..584c437 --- /dev/null +++ b/brand/bytefolk/README.md @@ -0,0 +1,46 @@ +# ByteFolk brand mark + +The ByteFolk organization mark is the minimal one-color **horned B** approved in +[issue #18](https://github.com/fullstack-ai-infra/.github/issues/18). It combines a capital `B`, +two modular horns, and an extended lower bowl that hints at the digital-employee mascot without +turning the logo into an illustration. + +## Assets + +| File | Use | +| --- | --- | +| `symbol.svg` | Primary mark on white or very light surfaces | +| `symbol-reversed.svg` | White mark on black or dark surfaces | +| `lockup.svg` | Horizontal symbol and `ByteFolk` wordmark | +| `avatar-1024.png` | Exact opaque upload artifact for the GitHub organization profile | + +The full-body mascot is a separate expressive brand character. It must not replace the symbol in +organization avatars, favicons, repository ownership marks, or compact product navigation. + +## Usage + +- Primary ink: `#141414`. +- Reversed ink: `#FFFFFF`. +- Keep clear space of at least one horn-stem width around the symbol. +- Use the standalone symbol at 16 px or larger. +- Use the horizontal lockup at 120 px wide or larger. +- Upload `avatar-1024.png` without recropping or recoloring it. The image already includes padding + for GitHub's circular presentation. +- On dark surfaces, use `symbol-reversed.svg`; do not place the primary black mark directly on a + dark background. + +Do not add gradients, shadows, outlines, background frames, eyes, nostrils, mane tiles, extra +colors, text inside the mark, or product-specific decoration. Do not stretch, rotate, crop, or +rearrange the horns and B geometry. + +## Reproduce the GitHub avatar + +ImageMagick is required for deterministic raster export: + +```bash +brand/bytefolk/render-avatar.sh +brand/bytefolk/render-avatar.sh --check +``` + +The first command rewrites `avatar-1024.png` from `symbol.svg`. The second renders to a temporary +file and verifies byte-for-byte equality with the tracked artifact. diff --git a/brand/bytefolk/avatar-1024.png b/brand/bytefolk/avatar-1024.png new file mode 100644 index 0000000000000000000000000000000000000000..b10c24e2260c0100c99446ce5af5787d21f95f67 GIT binary patch literal 8163 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 N&6~Dt%v + ByteFolk horizontal logo + The minimal ByteFolk horned B symbol paired with the ByteFolk wordmark. + + + + + + + + ByteFolk + diff --git a/brand/bytefolk/render-avatar.sh b/brand/bytefolk/render-avatar.sh new file mode 100755 index 0000000..511a90d --- /dev/null +++ b/brand/bytefolk/render-avatar.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +asset_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source_svg="$asset_dir/symbol.svg" +tracked_png="$asset_dir/avatar-1024.png" +mode="${1:-write}" + +if ! command -v magick >/dev/null 2>&1; then + echo "ImageMagick 'magick' is required to render the ByteFolk avatar." >&2 + exit 1 +fi + +render() { + local destination="$1" + magick \ + -background '#ffffff' \ + "$source_svg" \ + -filter box \ + -resize '1024x1024!' \ + -alpha remove \ + -alpha off \ + -colorspace sRGB \ + -type TrueColor \ + -strip \ + -define png:exclude-chunks=date,time \ + "PNG24:$destination" +} + +case "$mode" in + write) + render "$tracked_png" + echo "Rendered $tracked_png" + ;; + --check) + candidate="$(mktemp /tmp/bytefolk-avatar-check.XXXXXX)" + trap 'rm -f -- "$candidate"' EXIT + render "$candidate" + if ! cmp -s "$candidate" "$tracked_png"; then + echo "avatar-1024.png is stale; run brand/bytefolk/render-avatar.sh" >&2 + exit 1 + fi + echo "ByteFolk avatar matches symbol.svg" + ;; + *) + echo "Usage: brand/bytefolk/render-avatar.sh [write|--check]" >&2 + exit 2 + ;; +esac diff --git a/brand/bytefolk/symbol-reversed.svg b/brand/bytefolk/symbol-reversed.svg new file mode 100644 index 0000000..1e4f2ae --- /dev/null +++ b/brand/bytefolk/symbol-reversed.svg @@ -0,0 +1,10 @@ + + ByteFolk reversed symbol + A white version of the minimal ByteFolk symbol for dark backgrounds. + + + + + + + diff --git a/brand/bytefolk/symbol.svg b/brand/bytefolk/symbol.svg new file mode 100644 index 0000000..a0e4c7e --- /dev/null +++ b/brand/bytefolk/symbol.svg @@ -0,0 +1,10 @@ + + ByteFolk symbol + A minimal one-color capital B with two modular horns and an extended lower bowl. + + + + + + + diff --git a/scripts/validate-brand-assets.rb b/scripts/validate-brand-assets.rb new file mode 100644 index 0000000..b6ceb63 --- /dev/null +++ b/scripts/validate-brand-assets.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +require "pathname" +require "rexml/document" +require "zlib" + +ROOT = Pathname.new(File.expand_path("..", __dir__)).freeze +ASSET_DIR = ROOT.join("brand", "bytefolk").freeze +SVG_NAMES = %w[symbol.svg symbol-reversed.svg lockup.svg].freeze +PROHIBITED_ELEMENTS = %w[filter foreignObject image linearGradient radialGradient script].freeze + +errors = [] + +SVG_NAMES.each do |name| + path = ASSET_DIR.join(name) + unless path.file? + errors << "#{path.relative_path_from(ROOT)}: missing" + next + end + + begin + document = REXML::Document.new(path.read) + rescue REXML::ParseException => e + errors << "#{path.relative_path_from(ROOT)}: invalid XML: #{e.message.lines.first.strip}" + next + end + + root = document.root + unless root&.name == "svg" + errors << "#{path.relative_path_from(ROOT)}: root element must be svg" + next + end + errors << "#{path.relative_path_from(ROOT)}: viewBox must be present" if root.attributes["viewBox"].to_s.empty? + %w[width height].each do |attribute| + errors << "#{path.relative_path_from(ROOT)}: root #{attribute} is prohibited" if root.attributes[attribute] + end + + REXML::XPath.each(document, "//*") do |element| + errors << "#{path.relative_path_from(ROOT)}: <#{element.name}> is prohibited" if PROHIBITED_ELEMENTS.include?(element.name) + element.attributes.each_attribute do |attribute| + value = attribute.value.to_s + next if attribute.expanded_name == "xmlns" && value == "http://www.w3.org/2000/svg" + + if value.match?(/(?:https?:|data:|javascript:)/i) + errors << "#{path.relative_path_from(ROOT)}: external or executable attribute is prohibited" + end + end + end +end + +png_path = ASSET_DIR.join("avatar-1024.png") +if !png_path.file? + errors << "#{png_path.relative_path_from(ROOT)}: missing" +else + bytes = png_path.binread + signature = "\x89PNG\r\n\x1A\n".b + if bytes.byteslice(0, 8) != signature + errors << "#{png_path.relative_path_from(ROOT)}: invalid PNG signature" + else + offset = 8 + ihdr = nil + idat = +"".b + while offset + 12 <= bytes.bytesize + length = bytes.byteslice(offset, 4).unpack1("N") + type = bytes.byteslice(offset + 4, 4) + data = bytes.byteslice(offset + 8, length) + break unless data && offset + 12 + length <= bytes.bytesize + + ihdr = data if type == "IHDR" + idat << data if type == "IDAT" + offset += 12 + length + break if type == "IEND" + end + + if !ihdr || ihdr.bytesize != 13 + errors << "#{png_path.relative_path_from(ROOT)}: missing or invalid IHDR" + else + width, height, bit_depth, color_type, compression, filter_method, interlace = ihdr.unpack("NNCCCCC") + errors << "#{png_path.relative_path_from(ROOT)}: expected 1024x1024, got #{width}x#{height}" unless width == 1024 && height == 1024 + errors << "#{png_path.relative_path_from(ROOT)}: expected 8-bit RGB without alpha" unless bit_depth == 8 && color_type == 2 + errors << "#{png_path.relative_path_from(ROOT)}: unsupported PNG encoding" unless compression.zero? && filter_method.zero? && interlace.zero? + + if errors.empty? + raw = Zlib::Inflate.inflate(idat) + stride = width * 3 + expected_size = height * (stride + 1) + errors << "#{png_path.relative_path_from(ROOT)}: decompressed size mismatch" unless raw.bytesize == expected_size + + if raw.bytesize == expected_size + prior = Array.new(stride, 0) + pixels = [] + cursor = 0 + min_x = width + min_y = height + max_x = -1 + max_y = -1 + + height.times do |y| + filter = raw.getbyte(cursor) + cursor += 1 + encoded = raw.byteslice(cursor, stride).bytes + cursor += stride + row = Array.new(stride, 0) + + encoded.each_index do |index| + left = index >= 3 ? row[index - 3] : 0 + up = prior[index] + upper_left = index >= 3 ? prior[index - 3] : 0 + value = case filter + when 0 then encoded[index] + when 1 then encoded[index] + left + when 2 then encoded[index] + up + when 3 then encoded[index] + ((left + up) / 2) + when 4 + estimate = left + up - upper_left + distances = [(estimate - left).abs, (estimate - up).abs, (estimate - upper_left).abs] + encoded[index] + [left, up, upper_left][distances.index(distances.min)] + else + errors << "#{png_path.relative_path_from(ROOT)}: unsupported row filter #{filter}" + encoded[index] + end + row[index] = value & 0xff + end + + row.each_slice(3).with_index do |(red, green, blue), x| + unless red == green && green == blue + errors << "#{png_path.relative_path_from(ROOT)}: avatar must remain grayscale" + break + end + next if red >= 250 + + min_x = [min_x, x].min + min_y = [min_y, y].min + max_x = [max_x, x].max + max_y = [max_y, y].max + end + pixels.concat(row) + prior = row + end + + darkest = pixels.min + errors << "#{png_path.relative_path_from(ROOT)}: primary ink is not #141414" unless darkest == 20 + margins = [min_x, min_y, width - 1 - max_x, height - 1 - max_y] + if max_x.negative? || margins.any? { |margin| margin < 100 } + errors << "#{png_path.relative_path_from(ROOT)}: insufficient circular-crop padding #{margins.inspect}" + end + end + end + end + end +end + +if errors.empty? + puts "Validated #{SVG_NAMES.length} ByteFolk SVGs and one opaque 1024x1024 RGB avatar." +else + warn errors.join("\n") + exit 1 +end From 8cef9fc2815fe49ab38a03a6b31c804c6ce04c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:22:44 +0800 Subject: [PATCH 2/3] docs(#18): update brand issue link for ByteFolk Keep the approved R1 brand asset documentation aligned with the live organization handle. --- brand/bytefolk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brand/bytefolk/README.md b/brand/bytefolk/README.md index 584c437..fa642e7 100644 --- a/brand/bytefolk/README.md +++ b/brand/bytefolk/README.md @@ -1,7 +1,7 @@ # ByteFolk brand mark The ByteFolk organization mark is the minimal one-color **horned B** approved in -[issue #18](https://github.com/fullstack-ai-infra/.github/issues/18). It combines a capital `B`, +[issue #18](https://github.com/bytefolk/.github/issues/18). It combines a capital `B`, two modular horns, and an extended lower bowl that hints at the digital-employee mascot without turning the logo into an illustration. From 3c82dccf965dbcfa57ab23b384899632202445c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:52:46 +0800 Subject: [PATCH 3/3] fix(brand): keep avatar inside circular crop --- brand/bytefolk/README.md | 5 ++- brand/bytefolk/avatar-1024.png | Bin 8163 -> 8309 bytes brand/bytefolk/symbol-reversed.svg | 2 +- brand/bytefolk/symbol.svg | 2 +- scripts/validate-brand-assets.rb | 51 +++++++++++++++++++++++++++-- 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/brand/bytefolk/README.md b/brand/bytefolk/README.md index fa642e7..439b86f 100644 --- a/brand/bytefolk/README.md +++ b/brand/bytefolk/README.md @@ -22,10 +22,13 @@ organization avatars, favicons, repository ownership marks, or compact product n - Primary ink: `#141414`. - Reversed ink: `#FFFFFF`. - Keep clear space of at least one horn-stem width around the symbol. +- Keep every visible avatar pixel (any grayscale value below 250) inside the 496 px-radius circle + centered at `(511.5, 511.5)` on the 1024 px canvas. This leaves 16 px inside GitHub's 512 px + circular crop. - Use the standalone symbol at 16 px or larger. - Use the horizontal lockup at 120 px wide or larger. - Upload `avatar-1024.png` without recropping or recoloring it. The image already includes padding - for GitHub's circular presentation. + and a validated circular safe area for GitHub's presentation. - On dark surfaces, use `symbol-reversed.svg`; do not place the primary black mark directly on a dark background. diff --git a/brand/bytefolk/avatar-1024.png b/brand/bytefolk/avatar-1024.png index b10c24e2260c0100c99446ce5af5787d21f95f67..82e530bc6a10615e5bd08c7b24d2e6828ff3dd74 100644 GIT binary patch literal 8309 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 N&6~Dt%v ByteFolk reversed symbol A white version of the minimal ByteFolk symbol for dark backgrounds. - + diff --git a/brand/bytefolk/symbol.svg b/brand/bytefolk/symbol.svg index a0e4c7e..a522051 100644 --- a/brand/bytefolk/symbol.svg +++ b/brand/bytefolk/symbol.svg @@ -1,7 +1,7 @@ ByteFolk symbol A minimal one-color capital B with two modular horns and an extended lower bowl. - + diff --git a/scripts/validate-brand-assets.rb b/scripts/validate-brand-assets.rb index b6ceb63..beda490 100644 --- a/scripts/validate-brand-assets.rb +++ b/scripts/validate-brand-assets.rb @@ -8,8 +8,12 @@ ASSET_DIR = ROOT.join("brand", "bytefolk").freeze SVG_NAMES = %w[symbol.svg symbol-reversed.svg lockup.svg].freeze PROHIBITED_ELEMENTS = %w[filter foreignObject image linearGradient radialGradient script].freeze +CIRCULAR_SAFETY_MARGIN = 16.0 +INK_THRESHOLD = 250 errors = [] +circular_metrics = nil +geometry_signatures = {} SVG_NAMES.each do |name| path = ASSET_DIR.join(name) @@ -46,6 +50,21 @@ end end end + + symbol_group = REXML::XPath.first(document, "//*[starts-with(@id, 'bytefolk-symbol')]") + if symbol_group + child_geometry = symbol_group.elements.map do |element| + attributes = element.attributes.map { |key, value| [key, value] }.sort + [element.name, attributes] + end + geometry_signatures[name] = [symbol_group.attributes["transform"].to_s, child_geometry] + else + errors << "#{path.relative_path_from(ROOT)}: ByteFolk symbol group is missing" + end +end + +if geometry_signatures.length == SVG_NAMES.length && geometry_signatures.values.uniq.length != 1 + errors << "ByteFolk primary, reversed, and lockup symbol geometry must remain identical" end png_path = ASSET_DIR.join("avatar-1024.png") @@ -94,6 +113,11 @@ min_y = height max_x = -1 max_y = -1 + center_x = (width - 1) / 2.0 + center_y = (height - 1) / 2.0 + safe_radius = ([width, height].min / 2.0) - CIRCULAR_SAFETY_MARGIN + max_ink_radius = 0.0 + outside_circle_ink = 0 height.times do |y| filter = raw.getbyte(cursor) @@ -127,12 +151,15 @@ errors << "#{png_path.relative_path_from(ROOT)}: avatar must remain grayscale" break end - next if red >= 250 + next if red >= INK_THRESHOLD min_x = [min_x, x].min min_y = [min_y, y].min max_x = [max_x, x].max max_y = [max_y, y].max + ink_radius = Math.hypot(x - center_x, y - center_y) + max_ink_radius = [max_ink_radius, ink_radius].max + outside_circle_ink += 1 if ink_radius > safe_radius end pixels.concat(row) prior = row @@ -142,7 +169,26 @@ errors << "#{png_path.relative_path_from(ROOT)}: primary ink is not #141414" unless darkest == 20 margins = [min_x, min_y, width - 1 - max_x, height - 1 - max_y] if max_x.negative? || margins.any? { |margin| margin < 100 } - errors << "#{png_path.relative_path_from(ROOT)}: insufficient circular-crop padding #{margins.inspect}" + errors << "#{png_path.relative_path_from(ROOT)}: insufficient rectangular padding #{margins.inspect}" + end + if outside_circle_ink.positive? + errors << format( + "%s: %d ink pixels outside centered circular safe area (center=%.1f,%.1f radius=%.1fpx; max=%.1fpx)", + png_path.relative_path_from(ROOT), + outside_circle_ink, + center_x, + center_y, + safe_radius, + max_ink_radius + ) + else + circular_metrics = format( + "outside-circle ink=0; center=(%.1f,%.1f); safe radius=%.1fpx; max ink radius=%.1fpx", + center_x, + center_y, + safe_radius, + max_ink_radius + ) end end end @@ -152,6 +198,7 @@ if errors.empty? puts "Validated #{SVG_NAMES.length} ByteFolk SVGs and one opaque 1024x1024 RGB avatar." + puts "Circular crop: #{circular_metrics}." else warn errors.join("\n") exit 1