Skip to content

logos: optimise SVG images#2139

Open
mina86 wants to merge 1 commit intogit:gh-pagesfrom
mina86:a
Open

logos: optimise SVG images#2139
mina86 wants to merge 1 commit intogit:gh-pagesfrom
mina86:a

Conversation

@mina86
Copy link

@mina86 mina86 commented Mar 3, 2026

The main motivation for this change was unnecessary double point in the icon path. The top point of the right corner of the icon are in fact two separate points. I’ve used Inkscape’s ‘Join selected nodes’ function to merge them. I’ve then rounded coordinates in the path to two decimal points (again using Inkscape) and finally using a simple regex eliminate unnecessary spaces in the d attribute.

Working on that I've noticed that the clip paths don’t actually do anything: the paths do not go over the clip paths. I’ve thus removed the clip paths.

The styles for the path were over-specified. fill-opacity: 1, fill-rule: nonzero and stroke: none are all initial values of the properties, so I’ve stripped them out.

Finally, in the Logo images, the width attribute was specified without a unit. To match the height and the Icon images, I’ve added pt unit.

Cc: @jasonlong

The main motivation for this change was unnecessary double point in the
icon path.  The top point of the right corner of the icon are in fact
two separate points.  I’ve used Inkscape’s ‘Join selected nodes’
function to merge them.  I’ve then rounded coordinates in the path to
two decimal points (again using Inkscape) and finally using a simple
regex eliminate unnecessary spaces in the `d` attribute.

Working on that I've noticed that the clip paths don’t actually do
anything: the paths do not go over the clip paths.  I’ve thus removed
the clip paths.

The styles for the path were over-specified.  `fill-opacity: 1`,
`fill-rule: nonzero` and `stroke: none` are all initial values of the
properties, so I’ve stripped them out.

Finally, in the Logo images, the `width` attribute was specified without
a unit.  To match the `height` and the Icon images, I’ve added `pt`
unit.

Signed-off-by: Michal Nazarewicz <mina86@mina86.com>
@jasonlong
Copy link
Member

Seems fine to me, but also these things haven't been a problem in the past 14 years so... 🤷🏼. I'll leave it to the current maintainers to decide.

@mina86
Copy link
Author

mina86 commented Mar 3, 2026

I’ve only noticed because importing it to FontForge results in an ‘Open Contour’ warning.

@dscho
Copy link
Member

dscho commented Mar 8, 2026

Honestly, I don't quite see the point of this improvement... Why change something that served us well for so long? "Don't fix that which ain't broken..."

@mina86
Copy link
Author

mina86 commented Mar 8, 2026

Depends on what you consider ‘broken’. It does result in Open Contour error when importing the file in FontForge. While FF can deal with it, I figured might just as well fix it at the source. But yes, I agree that it’s not a big deal.

@dscho
Copy link
Member

dscho commented Mar 17, 2026

So I had another look, and I don't think that it's an optimization, really, to just round the decimals, optimization for what?

If it's optimization for making future changes or making this reusable in a nice way, then it would have been better to actually reconstruct the original design because clearly this was not designed standing on its tip, but it was designed rotated by 45°, on a regular grid, using non-rounded integers.

Unfortunately, I failed to make this work with an inverse clip path; That would have been the very cleanest solution. But that would have relied on non-standard inverse clip paths. Besides, even Inkscape messes this up because it cannot apply a clip-path correctly when its (non-rotated) group contains a rotated rectangle.

What I was able, however, was to reconstruct the original shapes and then construct a path that I find a lot nicer; It uses coordinates on the original grid as much as possible (path unions between rotated rectangles and circles partially ruin the beauty of this), then transforms the result into the desired size and rotation.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   width="92pt"
   height="92pt"
   viewBox="0 0 92 92"
   version="1.1"
   id="git-logo"
   xmlns:xlink="http://www.w3.org/1999/xlink"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg">
  <defs
     id="defs1">
    <rect
       style="fill:#f03c2e;fill-opacity:1"
       id="background"
       width="58"
       height="58"
       x="0"
       y="0.23803365"
       rx="5" />
    <g
       id="graph"
       style="fill:#ffffff;fill-opacity:1">
      <rect
         id="main-branch"
         width="5"
         height="41"
         x="38"
         y="0" />
      <rect
         id="topic-branch"
         width="5"
         height="32"
         x="38"
         y="17"
         transform="rotate(45,38,17)" />
      <circle
         id="branch-point"
         cx="40.5"
         cy="18"
         r="6" />
      <use
         x="0"
         y="0"
         xlink:href="#branch-point"
         id="main"
         transform="translate(0,23)" />
      <use
         x="0"
         y="0"
         xlink:href="#branch-point"
         id="topic"
         transform="translate(-23,23)" />
    </g>
  </defs>
  <path
     id="logo"
     style="fill:#f03c2e"
     d="M 5,0
        C 2.23,0 0,2.23 0,5
        V 53
        c 0,2.77 2.23,5 5,5
        H 53
        c 2.77,0 5,-2.23 5,-5
        V 5
        C 58,2.23 55.77,0 53,0
        H 43
        V 12.31
        A 6,6 0 0 1 46.5,17.76 6,6 0 0 1 43,23.22
        v 12.09
        A 6,6 0 0 1 46.5,40.77
        a 6,6 0 0 1 -6,6 6,6 0 0 1 -6,-6 6,6 0 0 1 3.5,-5.45
        V 23.83
        L 23.13,38.7
        a 6,6 0 0 1 0.37,2.06 6,6 0 0 1 -6,6 6,6 0 0 1 -6,-6 6,6 0 0 1 6,-6 6,6 0 0 1 2.11,0.39
        L 34.89,19.88
        A 6,6 0 0 1 34.5,17.76 6,6 0 0 1 38,12.31
        V 0
        Z"
     transform="scale(1.18) translate(-2 39) rotate(-45 0 0)" />
</svg>

The definitions are obviously not strictly necessary. But it's nice to have them for future reconstruction purposes, no?

@mina86
Copy link
Author

mina86 commented Mar 17, 2026

So I had another look, and I don't think that it's an optimization, really, to just round the decimals, optimization for what?

To reiterate, my main motivation was to convert the path to a closed one. Rounding of the decimals was done automatically by Inkscape and all other changes were just drive-by low-hanging fruit.

Unfortunately, I failed to make this work with an inverse clip path;

Another idea is to use a mask image, although neither Inkscape nor FontForge understands masks so this does the opposite of what I wanted in the first place. Probably not the best approach.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg id="git-logo"
     width="92pt" height="92pt" viewBox="0 0 78 78"
     xmlns="http://www.w3.org/2000/svg">
  <defs>
    <mask id="mask">
      <rect id="background"
            width="58" height="58" rx="5"
            fill="#fff" />
      <g id="graph-lines" stroke="#000" stroke-width="5">
        <line id="main-branch"
              x1="40.5" y1="-1"
              x2="40.5" y2="41" />
        <line id="topic-branch"
              x1="40.5" y1="18"
              x2="17.5" y2="41" />
      </g>
      <g id="branch-points" fill="#000">
        <circle id="branch-point"
                cx="40.5" cy="18" r="6" />
        <circle id="main"
                cx="40.5" cy="41" r="6" />
        <circle id="topic"
                cx="17.5" cy="41" r="6" />
      </g>
    </mask>
  </defs>
  <rect width="58" height="58" fill="#f03c2e"
        style="mask-image: url(#mask)"
        transform="rotate(-45 39 39) translate(10 10)" />
</svg>

Either way, I think using lines rather than rects for the lines is more readable since there’s no need for transformation. It’s also very natural to line up the coordinates of the line segments with coordinates with the circles.

The definitions are obviously not strictly necessary. But it's nice to have them for future reconstruction purposes, no?

Maybe a better approach is to have a separate source file, e.g. Git-Icon-Source.svg with the following contents:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="92pt" height="92pt" viewBox="0 0 78 78"
     id="git-logo" xmlns="http://www.w3.org/2000/svg">
  <rect id="background" fill="#f03c2e"
        x="10" y="10" width="58" height="58" rx="5" />
  <line id="main-branch" stroke="#fff" stroke-width="5"
        x1="50.5" y1="0"
        x2="50.5" y2="51" />
  <line id="topic-branch" stroke="#fff" stroke-width="5"
        x1="50.5" y1="28"
        x2="27.5" y2="51" />
  <circle id="branch-point" fill="#fff"
          cx="50.5" cy="28" r="6" />
  <circle id="main-start" fill="#fff"
          cx="50.5" cy="51" r="6" />
  <circle id="topic-start" fill="#fff"
          cx="27.5" cy="51" r="6" />
</svg>

and a script which generates other files:

import shlex
import subprocess

ACTIONS = (
        'file-open: Git-Icon-Source.svg',
        'select-by-element: line',
        'object-stroke-to-path',
        'selection-ungroup',
        'select-by-element: circle',
        'path-union',
        'select-by-element: rect',
        'path-difference',
        'transform-rotate: -45',
        'object-set-attribute: id,git-logo',
        'export-type: svg',
        'export-plain-svg',
        'object-set-attribute: fill,#f03c2e',
        'export-filename: Git-Icon-1788C.svg',
        'export-do',
        'object-set-attribute: fill,#000',
        'export-filename: Git-Icon-Black.svg',
        'export-do',
        'object-set-attribute: fill,#fff',
        'export-filename: Git-Icon-White.svg',
        'export-do',
)

def run(*cmd: str) -> None:
        print('+ ' + ' '.join(shlex.quote(s) for s in cmd))
        subprocess.check_call(cmd)

run('inkscape', '--actions=' + ';'.join(ACTIONS))

out_files = tuple(action[16:].strip()
                  for action in ACTIONS
                  if action.startswith('export-filename:'))
run('svgo', '-p2', '--multipass', *out_files)

Upside is that people downloading the icon get minified file. Downside is that the source isn’t discoverable; also the Inkscape actions are a bit fragile so if source file is changed the script might need to be updated as well.

@dscho
Copy link
Member

dscho commented Mar 17, 2026

@mina86 you have a good point here: Since there are so many near-duplicates that all need to be updated (in case of an update, that is, like the one to close the open contour), it makes sense to script that update.

Seeing that Inkscape is a quite hefty dependency, I looked for alternatives, that might fit into what git-scm.com already uses in its automation (Ruby and Node.JS, essentially), and found paper.js and Paperjs Offset (the latter needed to turn lines into paths). Here is the script:

// Generate the Git icon SVG using Paper.js boolean operations.
//
// Source geometry (on a 58x58 grid with origin at 0,0):
//   - Rounded rectangle background: (0,0) 58x58, corner radius 5
//   - Main branch: vertical line x=40.5, y=-1..41, stroke-width 5
//   - Topic branch: diagonal (40.5,18) to (17.5,41), stroke-width 5
//   - Three circles at (40.5,18), (40.5,41), (17.5,41), all r=6
//
// The result is placed into a 92x92 viewBox via:
//   transform="scale(1.179487) translate(10 10) rotate(-45 29 29)"
// Design on 58x58, translate to center in 78x78, rotate -45 around
// the shape center, then scale by 92/78 to fill the 92x92 viewBox.

const paper = require('paper');
const { PaperOffset } = require('paperjs-offset');
const fs = require('fs');
const path = require('path');

paper.setup(new paper.Size(78, 78));

// Background: rounded rectangle
const bg = new paper.Path.Rectangle({
  point: [0, 0],
  size: [58, 58],
  radius: 5,
});

// Branch lines (expand strokes into filled outlines)
const mainBranch = new paper.Path.Line({ from: [40.5, -1], to: [40.5, 41] });
const mainExp = PaperOffset.offsetStroke(mainBranch, 2.5, { cap: 'butt' });
mainBranch.remove();

const topicBranch = new paper.Path.Line({ from: [40.5, 18], to: [17.5, 41] });
const topicExp = PaperOffset.offsetStroke(topicBranch, 2.5, { cap: 'butt' });
topicBranch.remove();

// Circles
const branchPoint = new paper.Path.Circle({ center: [40.5, 18], radius: 6 });
const mainStart   = new paper.Path.Circle({ center: [40.5, 41], radius: 6 });
const topicStart  = new paper.Path.Circle({ center: [17.5, 41], radius: 6 });

// Unite all graph elements
let graph = mainExp.unite(topicExp);
graph = graph.unite(branchPoint);
graph = graph.unite(mainStart);
graph = graph.unite(topicStart);

// Subtract graph from background
const logo = bg.subtract(graph);

const pathData = logo.pathData;

// 92/78 scales the 78x78 intermediate space to fill the 92x92 viewBox
const transform =
  `scale(${+(92 / 78).toFixed(6)}) translate(10 10) rotate(-45 29 29)`;

const colors = {
  '1788C': '#f03c2e',
  'Black': '#100f0d',
  'White': '#fff',
};

const outDir = path.join(__dirname, 'static/images/logos/downloads');

for (const [variant, fill] of Object.entries(colors)) {
  const svg =
    `<svg xmlns="http://www.w3.org/2000/svg" width="92pt" height="92pt"` +
    ` viewBox="0 0 92 92"><path fill="${fill}"` +
    ` transform="${transform}" d="${pathData}"/></svg>`;

  const outPath = path.join(outDir, `Git-Icon-${variant}.svg`);
  fs.writeFileSync(outPath, svg);
  console.log(`Wrote ${outPath}`);
}

The output is e.g. this minified SVG: <svg xmlns="http://www.w3.org/2000/svg" width="92pt" height="92pt" viewBox="0 0 92 92"><path fill="#f03c2e" transform="scale(1.179487) translate(10 10) rotate(-45 29 29)" d="M5,58c-2.76142,0 -5,-2.23858 -5,-5v-48c0,-2.76142 2.23858,-5 5,-5l33,0v12.54404c-2.06553,0.94801 -3.5,3.03446 -3.5,5.45596c0,0.73514 0.13221,1.43941 0.37415,2.09031l-15.28384,15.28384c-0.6509,-0.24194 -1.35517,-0.37415 -2.09031,-0.37415c-3.31371,0 -6,2.68629 -6,6c0,3.31371 2.68629,6 6,6c3.31371,0 6,-2.68629 6,-6c0,-0.73514 -0.13221,-1.43941 -0.37415,-2.09031l14.87415,-14.87415l0,11.50851c-2.06553,0.94801 -3.5,3.03446 -3.5,5.45596c0,3.31371 2.68629,6 6,6c3.31371,0 6,-2.68629 6,-6c0,-2.42149 -1.43447,-4.50795 -3.5,-5.45596l0,-12.08808c2.06553,-0.94801 3.5,-3.03446 3.5,-5.45596c0,-2.42149 -1.43447,-4.50795 -3.5,-5.45596l0,-12.54404h10c2.76142,0 5,2.23858 5,5v48c0,2.76142 -2.23858,5 -5,5z"/></svg>

@mina86
Copy link
Author

mina86 commented Mar 17, 2026 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants