Skip to content

Commit

Permalink
Support output of SVG favicons
Browse files Browse the repository at this point in the history
  • Loading branch information
andy128k committed Oct 20, 2022
1 parent 8674547 commit 57abca6
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 9 deletions.
23 changes: 23 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,26 @@ function toPng(pipeline: sharp.Sharp): Promise<Buffer> {
return pipeline.png().toBuffer();
}

async function createSvg(
sourceset: SourceImage[],
options: IconPlaneOptions
): Promise<Buffer> {
const { width, height } = options;
const source = bestSource(sourceset, width, height);
if (source.metadata.format === "svg") {
return source.data;
} else {
const pipeline = await createPlane(sourceset, options);
const png = await toPng(pipeline);
const encodedPng = png.toString("base64");
return Buffer.from(
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
<image width="${width}" height="${height}" xlink:href="data:image/png;base64,${encodedPng}"/>
</svg>`
);
}
}

export async function createFavicon(
sourceset: SourceImage[],
name: string,
Expand All @@ -248,6 +268,9 @@ export async function createFavicon(
);
const contents = toIco(images);
return { name, contents };
} else if (path.extname(name) === ".svg") {
const contents = await createSvg(sourceset, properties[0]);
return { name, contents };
} else {
const contents = await createPlane(sourceset, properties[0]).then(toPng);
return { name, contents };
Expand Down
18 changes: 12 additions & 6 deletions src/platforms/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@ import {
} from "../config/defaults";
import { asString, createFavicon, relativeTo, SourceImage } from "../helpers";

export interface OptionalMixin {
readonly optional?: boolean;
}

export function uniformIconOptions<T extends NamedIconOptions>(
options: FaviconOptions,
iconsChoice:
| IconOptions
| boolean
| (string | NamedIconOptions)[]
| undefined,
platformConfig: T[]
platformConfig: (T & OptionalMixin)[]
): T[] {
let result = [];
if (Array.isArray(iconsChoice)) {
Expand All @@ -36,12 +40,14 @@ export function uniformIconOptions<T extends NamedIconOptions>(
...iconsChoices[iconOptions.name],
}));
} else if (typeof iconsChoice === "object") {
result = platformConfig.map((iconOptions) => ({
...iconOptions,
...iconsChoice,
}));
result = platformConfig
.filter((iconOptions) => !iconOptions.optional)
.map((iconOptions) => ({
...iconOptions,
...iconsChoice,
}));
} else {
result = platformConfig;
result = platformConfig.filter((iconOptions) => !iconOptions.optional);
}

return result.map((iconOptions) => ({
Expand Down
8 changes: 6 additions & 2 deletions src/platforms/favicons.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { FaviconHtmlElement } from "../index";
import { FaviconOptions, NamedIconOptions } from "../config/defaults";
import { transparentIcon, transparentIcons } from "../config/icons";
import { Platform, uniformIconOptions } from "./base";
import { OptionalMixin, Platform, uniformIconOptions } from "./base";

const ICONS_OPTIONS: NamedIconOptions[] = [
const ICONS_OPTIONS: (NamedIconOptions & OptionalMixin)[] = [
{ name: "favicon.ico", ...transparentIcons(16, 24, 32, 48, 64) },
{ name: "favicon-16x16.png", ...transparentIcon(16) },
{ name: "favicon-32x32.png", ...transparentIcon(32) },
{ name: "favicon-48x48.png", ...transparentIcon(48) },
{ name: "favicon.svg", ...transparentIcon(1024), optional: true }, // arbitrary size. if more than one svg source is given, the closest to this size will be picked.
];

export class FaviconsPlatform extends Platform {
Expand All @@ -23,6 +24,9 @@ export class FaviconsPlatform extends Platform {
if (name.endsWith(".ico")) {
// prettier-ignore
return `<link rel="icon" type="image/x-icon" href="${this.relative(name)}">`;
} else if (name.endsWith(".svg")) {
// prettier-ignore
return `<link rel="icon" type="image/svg+xml" href="${this.relative(name)}">`;
}

const { width, height } = options.sizes[0];
Expand Down
36 changes: 36 additions & 0 deletions test/__snapshots__/svgOutput.test.js.snap

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion test/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,17 @@ async function imagePlanes(image) {
return result;
}

function isSvg(image) {
return path.extname(image.name) === ".svg";
}

expect.extend({
async toMatchFaviconsSnapshot(received) {
for (const image of received.images) {
if (isSvg(image)) {
continue;
}

const planes = await imagePlanes(image);

for (const plane of planes) {
Expand All @@ -46,7 +54,7 @@ expect.extend({
...received,
images: received.images.map((image) => ({
...image,
contents: null,
contents: isSvg(image) ? image.contents.toString("utf-8") : null,
})),
};

Expand Down
36 changes: 36 additions & 0 deletions test/svgOutput.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import favicons from "../src";
import { logo_png, logo_svg } from "./util";

test("should reuse svg as a favicon", async () => {
expect.assertions(1);

const result = await favicons(logo_svg, {
icons: {
favicons: ["favicon.svg"],
android: false,
appleIcon: false,
appleStartup: false,
windows: false,
yandex: false,
},
});

await expect(result).toMatchFaviconsSnapshot();
});

test("should generate svg favicon", async () => {
expect.assertions(1);

const result = await favicons(logo_png, {
icons: {
favicons: ["favicon.svg"],
android: false,
appleIcon: false,
appleStartup: false,
windows: false,
yandex: false,
},
});

await expect(result).toMatchFaviconsSnapshot();
});

0 comments on commit 57abca6

Please sign in to comment.