DEV Community

Daniel Pertu
Daniel Pertu

Posted on

One mug, four files: our favicon.ico is a PNG, and two of the SVGs differ by a line that changes nothing

Our logo is a beer mug: a tapered body, a handle made of one stroked path, and four white ellipses for the foam. Call it fifteen elements. It is in the repository four times, in four different shapes, and I only went looking because two of the files are exactly the same size.

$ curl -s -o /dev/null -w '%{size_download} %{content_type}\n' https://pub-trivia.app/logo.svg
606 image/svg+xml
$ curl -s -o /dev/null -w '%{size_download} %{content_type}\n' https://pub-trivia.app/favicon.svg
606 image/svg+xml
Enter fullscreen mode Exit fullscreen mode

Same byte count, different content:

$ curl -s https://pub-trivia.app/logo.svg    | md5
efd30e469953020c2576fb7ae094d65f
$ curl -s https://pub-trivia.app/favicon.svg | md5
d7e66559089a819bb24ee70f9f634f9a
Enter fullscreen mode Exit fullscreen mode

Both are public, so you can run those two commands yourself. The whole diff is this:

   <ellipse cx="30" cy="33" rx="7" ry="8" fill="white" />
   <ellipse cx="45" cy="29" rx="9" ry="10" fill="white" />
-  <ellipse cx="60" cy="32" rx="7" ry="8" fill="white" />
   <ellipse cx="45" cy="36" rx="18" ry="6" fill="white" />
+  <ellipse cx="60" cy="32" rx="7" ry="8" fill="white" />
Enter fullscreen mode Exit fullscreen mode

Two sibling ellipses, swapped. SVG paints in document order, so swapping two siblings changes which one is on top, which is a real difference in general and no difference at all here, because both are fill="white" with no stroke and no opacity. Two files, two hashes, one picture. The third copy, icon.svg, keeps the same order as favicon.svg, so the fork happened in logo.svg.

What I cannot tell you is which file is the original, which is the correction, or what the swap was for. There is no comment, the two files have never been edited since, and the difference is unobservable, which is precisely why it was never going to be noticed.

The third copy has arithmetic in it

icon.svg is the only one of the three that is not just the mug. It is 1,034 bytes because it does three things the other two do not:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
  <rect width="512" height="512" fill="#171717" />

  <!--
    Original mug bounding box: x=18 to x=86 (w=68), y=19 to y=92 (h=73)
    Original center: x=52, y=55.5
    Scale 3.6x so the mug fits ~245x263px, well inside the 410px safe zone
    Translate the center of the mug to the canvas center (256, 256)
  -->
  <g transform="translate(256, 256) scale(3.6) translate(-52, -55.5)">
Enter fullscreen mode Exit fullscreen mode

A 512 canvas, a dark plate behind the mug, and a transform that centres a drawing whose own coordinate system starts at 18,19. The three translate-scale-translate steps are the standard trick for scaling about an arbitrary point, and the comment is the only place in the repository where the numbers are written down.

The 410 pixel safe zone is why the file exists at all. It is the installed-app icon, and the web app manifest declares exactly one:

$ curl -s https://pub-trivia.app/manifest.webmanifest
{"name":"PubTrivia", ... "icons":[{"src":"/icon.svg","sizes":"any","type":"image/svg+xml","purpose":"maskable"}]}
Enter fullscreen mode Exit fullscreen mode

purpose: "maskable" means the platform may crop the icon to any shape it likes: a circle, a squircle, a rounded square. The guaranteed-visible region is the central circle of 80 per cent of the width, 410 pixels of 512, and anything outside it can be cut. That is also why the dark rectangle is in the file rather than being left transparent. A maskable icon with a transparent background gets an operating-system-chosen background behind it, and the mug's foam is white.

The fourth copy is not an SVG at all

$ file app/favicon.ico
app/favicon.ico: PNG image data, 200 x 200, 8-bit/color RGBA, non-interlaced
Enter fullscreen mode Exit fullscreen mode

It is a PNG. It has been a PNG the whole time. The extension says otherwise, Next's file convention for app/favicon.ico picked it up anyway, and the live HTML advertises it like this:

<link rel="shortcut icon" href="/favicon.svg"/>
<link rel="icon" href="/favicon.ico?favicon.1s1wf25m_8_hs.ico" sizes="200x200" type="image/x-icon"/>
<link rel="icon" href="/favicon.svg"/>
Enter fullscreen mode Exit fullscreen mode

The sizes="200x200" is the interesting part. Next read the dimensions out of the file to write that attribute, so it parsed a PNG header while labelling the result image/x-icon. 200 by 200 is also not a size anybody would choose for an ICO, which normally carries 16, 32 and 48 pixel frames in one container. The response goes further:

$ curl -sI https://pub-trivia.app/favicon.ico | grep -i -e content-type -e x-content-type
content-type: image/vnd.microsoft.icon
x-content-type-options: nosniff
Enter fullscreen mode Exit fullscreen mode

PNG bytes, a Microsoft ICO content type, and a header telling the browser not to work out the type for itself. Nothing is broken by it: a PNG inside a .ico has been accepted for about fifteen years, and favicon decoding goes by the bytes rather than by the declared type. I would still rather it were honest. Decoding the pixels confirms it is the same drawing as the other three, amber #F5A623 body, white foam, fully transparent background, with antialiasing on the curves.

So: three links to two files for the browser tab, one more for iOS, and a fifth reference in the manifest. The duplicate rel="icon" plus rel="shortcut icon" for the same SVG comes from declaring both icon and shortcut in the metadata object, and shortcut icon has been a legacy spelling for a long time.

Why four files is not obviously wrong

The reason to collapse them is the usual one: a logo has one shape, and four copies of a shape is three chances to change three of them.

The reason not to is that each of the four files answers a different question. The tab icon has to be legible at 16 pixels, where the handle is roughly one pixel wide. The installed-app icon has to survive being cropped to a circle and needs its own background. The in-page logo sits on a dark page and needs no plate at all. And the raster file exists for the places that still will not take a vector.

The one that does not survive that defence is the pair that differs by a reordered line. Those two really are the same picture with two hashes, and the only reason to keep both is that /favicon.svg is a conventional URL and /logo.svg is the one twelve files in the repository reference, including the link-preview card generator, which reads the file off disk at build time and inlines it as a base64 data URI because Satori will not rasterise a raw <svg> child. A symlink would do, except that the two files are no longer identical, which is how a meaningless difference becomes load bearing.

You can see all four in about thirty seconds: /logo.svg and /favicon.svg are the pair, /icon.svg is the one on the dark plate, /favicon.ico is the PNG, and the mug appears once more, inlined, on the generated link preview. The drawing is the same every time, which is the only part of this that went right.

Top comments (0)