@mrdemonwolf/iconwolf 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +15 -3
- package/dist/generator.js.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/lib.d.ts +2 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -0
- package/dist/lib.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/banner.d.ts +5 -0
- package/dist/utils/banner.d.ts.map +1 -0
- package/dist/utils/banner.js +88 -0
- package/dist/utils/banner.js.map +1 -0
- package/dist/utils/icon-composer.d.ts +1 -0
- package/dist/utils/icon-composer.d.ts.map +1 -1
- package/dist/utils/icon-composer.js +24 -2
- package/dist/utils/icon-composer.js.map +1 -1
- package/package.json +1 -1
- package/src/generator.ts +17 -3
- package/src/index.ts +19 -2
- package/src/lib.ts +7 -0
- package/src/types.ts +13 -0
- package/src/utils/banner.ts +116 -0
- package/src/utils/icon-composer.ts +37 -3
- package/tests/cli.test.ts +11 -0
- package/tests/generator.test.ts +48 -0
- package/tests/utils/banner.test.ts +142 -0
package/dist/generator.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAgCrE,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAgI7B"}
|
package/dist/generator.js
CHANGED
|
@@ -8,19 +8,21 @@ import { generateStandardIcon } from './variants/standard.js';
|
|
|
8
8
|
import { generateFavicon } from './variants/favicon.js';
|
|
9
9
|
import { generateSplashIcon } from './variants/splash.js';
|
|
10
10
|
import { generateAndroidIcons } from './variants/android.js';
|
|
11
|
+
import { applyBanner, shouldApplyBanner } from './utils/banner.js';
|
|
11
12
|
async function resolveInput(resolvedPath, bgColor, silent) {
|
|
12
13
|
if (isIconComposerFolder(resolvedPath)) {
|
|
13
14
|
if (!silent)
|
|
14
15
|
logger.info(`Apple Icon Composer file: ${resolvedPath}`);
|
|
15
16
|
const result = await renderIconComposerFolder(resolvedPath);
|
|
16
17
|
const inputPath = result.composedImagePath;
|
|
18
|
+
const foregroundPath = result.foregroundImagePath;
|
|
17
19
|
const cleanupPath = path.dirname(result.composedImagePath);
|
|
18
20
|
if (bgColor === '#FFFFFF') {
|
|
19
21
|
bgColor = result.extractedBgColor;
|
|
20
22
|
if (!silent)
|
|
21
23
|
logger.info(`Extracted background color: ${bgColor}`);
|
|
22
24
|
}
|
|
23
|
-
return { inputPath, cleanupPath, bgColor };
|
|
25
|
+
return { inputPath, foregroundPath, cleanupPath, bgColor };
|
|
24
26
|
}
|
|
25
27
|
return { inputPath: resolvedPath, bgColor };
|
|
26
28
|
}
|
|
@@ -37,6 +39,7 @@ export async function generate(options) {
|
|
|
37
39
|
throw new Error(`Source not found: ${resolvedInput}`);
|
|
38
40
|
}
|
|
39
41
|
let inputPath;
|
|
42
|
+
let foregroundPath;
|
|
40
43
|
let cleanupPath;
|
|
41
44
|
let splashPath;
|
|
42
45
|
let splashCleanupPath;
|
|
@@ -44,6 +47,7 @@ export async function generate(options) {
|
|
|
44
47
|
// Resolve main input (Apple Icon Composer .icon folder or PNG)
|
|
45
48
|
const mainInput = await resolveInput(resolvedInput, bgColor, silent);
|
|
46
49
|
inputPath = mainInput.inputPath;
|
|
50
|
+
foregroundPath = mainInput.foregroundPath;
|
|
47
51
|
cleanupPath = mainInput.cleanupPath;
|
|
48
52
|
bgColor = mainInput.bgColor;
|
|
49
53
|
// Resolve splash input if provided
|
|
@@ -53,7 +57,7 @@ export async function generate(options) {
|
|
|
53
57
|
throw new Error(`Splash source not found: ${resolvedSplashInput}`);
|
|
54
58
|
}
|
|
55
59
|
const splashInput = await resolveInput(resolvedSplashInput, bgColor, silent);
|
|
56
|
-
splashPath = splashInput.inputPath;
|
|
60
|
+
splashPath = splashInput.foregroundPath ?? splashInput.inputPath;
|
|
57
61
|
splashCleanupPath = splashInput.cleanupPath;
|
|
58
62
|
}
|
|
59
63
|
// Validate source image
|
|
@@ -100,11 +104,19 @@ export async function generate(options) {
|
|
|
100
104
|
logger.generated(result);
|
|
101
105
|
}
|
|
102
106
|
if (generateAll || variants.splash) {
|
|
103
|
-
const result = await generateSplashIcon(splashPath || inputPath, outputDir);
|
|
107
|
+
const result = await generateSplashIcon(splashPath || foregroundPath || inputPath, outputDir);
|
|
104
108
|
results.push(result);
|
|
105
109
|
if (!silent)
|
|
106
110
|
logger.generated(result);
|
|
107
111
|
}
|
|
112
|
+
// Apply banner overlay if requested
|
|
113
|
+
if (options.banner) {
|
|
114
|
+
for (const result of results) {
|
|
115
|
+
if (shouldApplyBanner(result.filePath)) {
|
|
116
|
+
await applyBanner(result, options.banner);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
108
120
|
if (!silent)
|
|
109
121
|
logger.summary(results);
|
|
110
122
|
return results;
|
package/dist/generator.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EACL,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { generate } from './generator.js';
|
|
|
4
4
|
import * as logger from './utils/logger.js';
|
|
5
5
|
import { resolveDefaultOutputDir } from './utils/paths.js';
|
|
6
6
|
import { readCachedUpdateInfo, refreshCacheInBackground, } from './utils/update-notifier.js';
|
|
7
|
-
const VERSION = '0.1
|
|
7
|
+
const VERSION = '0.2.1';
|
|
8
8
|
const program = new Command();
|
|
9
9
|
program
|
|
10
10
|
.name('iconwolf')
|
|
@@ -18,6 +18,9 @@ program
|
|
|
18
18
|
.option('--icon', 'Generate standard icon.png only')
|
|
19
19
|
.option('--splash-input <path>', 'Use a separate image for the splash screen icon')
|
|
20
20
|
.option('--bg-color <hex>', 'Background color for Android adaptive icon', '#FFFFFF')
|
|
21
|
+
.option('--banner <text>', 'Diagonal ribbon banner text (e.g. DEV, BETA, STAGING)')
|
|
22
|
+
.option('--banner-color <hex>', 'Ribbon color (default: auto from text)')
|
|
23
|
+
.option('--banner-position <pos>', 'Banner position: top-left, top-right, bottom-left, bottom-right', 'top-left')
|
|
21
24
|
.action(async (input, opts) => {
|
|
22
25
|
const updateInfo = readCachedUpdateInfo(VERSION);
|
|
23
26
|
refreshCacheInBackground().catch(() => { });
|
|
@@ -32,6 +35,13 @@ program
|
|
|
32
35
|
},
|
|
33
36
|
bgColor: opts.bgColor,
|
|
34
37
|
splashInputPath: opts.splashInput,
|
|
38
|
+
banner: opts.banner
|
|
39
|
+
? {
|
|
40
|
+
text: opts.banner,
|
|
41
|
+
color: opts.bannerColor,
|
|
42
|
+
position: opts.bannerPosition,
|
|
43
|
+
}
|
|
44
|
+
: undefined,
|
|
35
45
|
};
|
|
36
46
|
try {
|
|
37
47
|
await generate(options);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EACL,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AAGpC,MAAM,OAAO,GAAG,OAAO,CAAC;AAExB,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,UAAU,CAAC;KAChB,WAAW,CACV,+GAA+G,CAChH;KACA,OAAO,CAAC,OAAO,CAAC;KAChB,QAAQ,CACP,SAAS,EACT,6DAA6D,CAC9D;KACA,MAAM,CAAC,oBAAoB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,CAAC;KAC3E,MAAM,CAAC,WAAW,EAAE,8CAA8C,CAAC;KACnE,MAAM,CAAC,WAAW,EAAE,2BAA2B,CAAC;KAChD,MAAM,CAAC,UAAU,EAAE,kCAAkC,CAAC;KACtD,MAAM,CAAC,QAAQ,EAAE,iCAAiC,CAAC;KACnD,MAAM,CACL,uBAAuB,EACvB,iDAAiD,CAClD;KACA,MAAM,CACL,kBAAkB,EAClB,4CAA4C,EAC5C,SAAS,CACV;KACA,MAAM,CAAC,KAAK,EAAE,KAAa,EAAE,IAAI,EAAE,EAAE;IACpC,MAAM,UAAU,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;IACjD,wBAAwB,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAE3C,MAAM,OAAO,GAAqB;QAChC,SAAS,EAAE,KAAK;QAChB,SAAS,EAAE,IAAI,CAAC,MAAM;QACtB,QAAQ,EAAE;YACR,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,KAAK;YAC9B,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,KAAK;YAC9B,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,KAAK;YAC5B,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,KAAK;SACzB;QACD,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,eAAe,EAAE,IAAI,CAAC,WAAW;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EACL,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AAGpC,MAAM,OAAO,GAAG,OAAO,CAAC;AAExB,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,UAAU,CAAC;KAChB,WAAW,CACV,+GAA+G,CAChH;KACA,OAAO,CAAC,OAAO,CAAC;KAChB,QAAQ,CACP,SAAS,EACT,6DAA6D,CAC9D;KACA,MAAM,CAAC,oBAAoB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,CAAC;KAC3E,MAAM,CAAC,WAAW,EAAE,8CAA8C,CAAC;KACnE,MAAM,CAAC,WAAW,EAAE,2BAA2B,CAAC;KAChD,MAAM,CAAC,UAAU,EAAE,kCAAkC,CAAC;KACtD,MAAM,CAAC,QAAQ,EAAE,iCAAiC,CAAC;KACnD,MAAM,CACL,uBAAuB,EACvB,iDAAiD,CAClD;KACA,MAAM,CACL,kBAAkB,EAClB,4CAA4C,EAC5C,SAAS,CACV;KACA,MAAM,CACL,iBAAiB,EACjB,uDAAuD,CACxD;KACA,MAAM,CAAC,sBAAsB,EAAE,wCAAwC,CAAC;KACxE,MAAM,CACL,yBAAyB,EACzB,iEAAiE,EACjE,UAAU,CACX;KACA,MAAM,CAAC,KAAK,EAAE,KAAa,EAAE,IAAI,EAAE,EAAE;IACpC,MAAM,UAAU,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;IACjD,wBAAwB,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAE3C,MAAM,OAAO,GAAqB;QAChC,SAAS,EAAE,KAAK;QAChB,SAAS,EAAE,IAAI,CAAC,MAAM;QACtB,QAAQ,EAAE;YACR,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,KAAK;YAC9B,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,KAAK;YAC9B,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,KAAK;YAC5B,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,KAAK;SACzB;QACD,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,eAAe,EAAE,IAAI,CAAC,WAAW;QACjC,MAAM,EAAE,IAAI,CAAC,MAAM;YACjB,CAAC,CAAC;gBACE,IAAI,EAAE,IAAI,CAAC,MAAM;gBACjB,KAAK,EAAE,IAAI,CAAC,WAAW;gBACvB,QAAQ,EAAE,IAAI,CAAC,cAAgC;aAChD;YACH,CAAC,CAAC,SAAS;KACd,CAAC;IAEF,IAAI,CAAC;QACH,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;QAExB,IAAI,UAAU,EAAE,eAAe,EAAE,CAAC;YAChC,MAAM,CAAC,YAAY,CACjB,UAAU,CAAC,cAAc,EACzB,UAAU,CAAC,aAAa,CACzB,CAAC;QACJ,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,KAAK,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
|
package/dist/lib.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ export { generateFavicon } from './variants/favicon.js';
|
|
|
4
4
|
export { generateSplashIcon } from './variants/splash.js';
|
|
5
5
|
export { generateAndroidIcons } from './variants/android.js';
|
|
6
6
|
export { validateSourceImage } from './utils/image.js';
|
|
7
|
+
export { applyBanner, createBannerSvg, shouldApplyBanner, } from './utils/banner.js';
|
|
7
8
|
export { isIconComposerFolder, renderIconComposerFolder, } from './utils/icon-composer.js';
|
|
8
9
|
export { OUTPUT_FILES, resolveOutputPath } from './utils/paths.js';
|
|
9
|
-
export type { VariantFlags, GeneratorOptions, GenerationResult, } from './types.js';
|
|
10
|
+
export type { BannerOptions, BannerPosition, VariantFlags, GeneratorOptions, GenerationResult, } from './types.js';
|
|
10
11
|
//# sourceMappingURL=lib.d.ts.map
|
package/dist/lib.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EACL,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACnE,YAAY,EACV,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EACL,WAAW,EACX,eAAe,EACf,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACnE,YAAY,EACV,aAAa,EACb,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
|
package/dist/lib.js
CHANGED
|
@@ -4,6 +4,7 @@ export { generateFavicon } from './variants/favicon.js';
|
|
|
4
4
|
export { generateSplashIcon } from './variants/splash.js';
|
|
5
5
|
export { generateAndroidIcons } from './variants/android.js';
|
|
6
6
|
export { validateSourceImage } from './utils/image.js';
|
|
7
|
+
export { applyBanner, createBannerSvg, shouldApplyBanner, } from './utils/banner.js';
|
|
7
8
|
export { isIconComposerFolder, renderIconComposerFolder, } from './utils/icon-composer.js';
|
|
8
9
|
export { OUTPUT_FILES, resolveOutputPath } from './utils/paths.js';
|
|
9
10
|
//# sourceMappingURL=lib.js.map
|
package/dist/lib.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lib.js","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EACL,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"lib.js","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EACL,WAAW,EACX,eAAe,EACf,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export type BannerPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
2
|
+
export interface BannerOptions {
|
|
3
|
+
text: string;
|
|
4
|
+
color?: string;
|
|
5
|
+
position?: BannerPosition;
|
|
6
|
+
}
|
|
1
7
|
export interface VariantFlags {
|
|
2
8
|
android: boolean;
|
|
3
9
|
favicon: boolean;
|
|
@@ -10,6 +16,7 @@ export interface GeneratorOptions {
|
|
|
10
16
|
variants: VariantFlags;
|
|
11
17
|
bgColor: string;
|
|
12
18
|
splashInputPath?: string;
|
|
19
|
+
banner?: BannerOptions;
|
|
13
20
|
silent?: boolean;
|
|
14
21
|
}
|
|
15
22
|
export interface GenerationResult {
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GACtB,UAAU,GACV,WAAW,GACX,aAAa,GACb,cAAc,CAAC;AAEnB,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BannerOptions, BannerPosition, GenerationResult } from '../types.js';
|
|
2
|
+
export declare function createBannerSvg(size: number, text: string, color: string, position: BannerPosition): string;
|
|
3
|
+
export declare function applyBanner(result: GenerationResult, bannerOptions: BannerOptions): Promise<void>;
|
|
4
|
+
export declare function shouldApplyBanner(filePath: string): boolean;
|
|
5
|
+
//# sourceMappingURL=banner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"banner.d.ts","sourceRoot":"","sources":["../../src/utils/banner.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,gBAAgB,EACjB,MAAM,aAAa,CAAC;AAgBrB,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,cAAc,GACvB,MAAM,CA0CR;AAUD,wBAAsB,WAAW,CAC/B,MAAM,EAAE,gBAAgB,EACxB,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC,IAAI,CAAC,CAqBf;AASD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE3D"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
|
+
const DEFAULT_COLORS = {
|
|
5
|
+
DEV: '#4CAF50',
|
|
6
|
+
BETA: '#FF9800',
|
|
7
|
+
STAGING: '#2196F3',
|
|
8
|
+
ALPHA: '#9C27B0',
|
|
9
|
+
};
|
|
10
|
+
const FALLBACK_COLOR = '#F44336';
|
|
11
|
+
function resolveColor(text, color) {
|
|
12
|
+
if (color)
|
|
13
|
+
return color;
|
|
14
|
+
return DEFAULT_COLORS[text.toUpperCase()] ?? FALLBACK_COLOR;
|
|
15
|
+
}
|
|
16
|
+
export function createBannerSvg(size, text, color, position) {
|
|
17
|
+
const ribbonWidth = Math.round(size * 0.42);
|
|
18
|
+
const ribbonHeight = Math.round(size * 0.08);
|
|
19
|
+
const fontSize = Math.round(ribbonHeight * 0.6);
|
|
20
|
+
const halfDiag = ribbonWidth / 2;
|
|
21
|
+
// Center of the ribbon in each corner
|
|
22
|
+
let cx;
|
|
23
|
+
let cy;
|
|
24
|
+
let angle;
|
|
25
|
+
switch (position) {
|
|
26
|
+
case 'top-left':
|
|
27
|
+
cx = size * 0.2;
|
|
28
|
+
cy = size * 0.2;
|
|
29
|
+
angle = -45;
|
|
30
|
+
break;
|
|
31
|
+
case 'top-right':
|
|
32
|
+
cx = size * 0.8;
|
|
33
|
+
cy = size * 0.2;
|
|
34
|
+
angle = 45;
|
|
35
|
+
break;
|
|
36
|
+
case 'bottom-left':
|
|
37
|
+
cx = size * 0.2;
|
|
38
|
+
cy = size * 0.8;
|
|
39
|
+
angle = 45;
|
|
40
|
+
break;
|
|
41
|
+
case 'bottom-right':
|
|
42
|
+
cx = size * 0.8;
|
|
43
|
+
cy = size * 0.8;
|
|
44
|
+
angle = -45;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
|
48
|
+
<g transform="translate(${cx}, ${cy}) rotate(${angle})">
|
|
49
|
+
<rect x="${-halfDiag}" y="${-ribbonHeight / 2}" width="${ribbonWidth}" height="${ribbonHeight}" fill="${color}"/>
|
|
50
|
+
<text x="0" y="0" text-anchor="middle" dominant-baseline="central"
|
|
51
|
+
font-family="Arial, Helvetica, sans-serif" font-weight="bold"
|
|
52
|
+
font-size="${fontSize}" fill="white">${escapeXml(text)}</text>
|
|
53
|
+
</g>
|
|
54
|
+
</svg>`;
|
|
55
|
+
}
|
|
56
|
+
function escapeXml(s) {
|
|
57
|
+
return s
|
|
58
|
+
.replace(/&/g, '&')
|
|
59
|
+
.replace(/</g, '<')
|
|
60
|
+
.replace(/>/g, '>')
|
|
61
|
+
.replace(/"/g, '"');
|
|
62
|
+
}
|
|
63
|
+
export async function applyBanner(result, bannerOptions) {
|
|
64
|
+
const color = resolveColor(bannerOptions.text, bannerOptions.color);
|
|
65
|
+
const position = bannerOptions.position ?? 'top-left';
|
|
66
|
+
const meta = await sharp(result.filePath).metadata();
|
|
67
|
+
const size = meta.width ?? 1024;
|
|
68
|
+
const svg = createBannerSvg(size, bannerOptions.text, color, position);
|
|
69
|
+
const svgBuffer = Buffer.from(svg);
|
|
70
|
+
const tmpPath = result.filePath + '.tmp';
|
|
71
|
+
await sharp(result.filePath)
|
|
72
|
+
.composite([{ input: svgBuffer, top: 0, left: 0 }])
|
|
73
|
+
.png()
|
|
74
|
+
.toFile(tmpPath);
|
|
75
|
+
fs.renameSync(tmpPath, result.filePath);
|
|
76
|
+
const stat = fs.statSync(result.filePath);
|
|
77
|
+
result.size = stat.size;
|
|
78
|
+
}
|
|
79
|
+
/** Files that should not receive a banner overlay. */
|
|
80
|
+
const SKIP_FILES = new Set([
|
|
81
|
+
'favicon.png',
|
|
82
|
+
'android-icon-background.png',
|
|
83
|
+
'monochrome-icon.png',
|
|
84
|
+
]);
|
|
85
|
+
export function shouldApplyBanner(filePath) {
|
|
86
|
+
return !SKIP_FILES.has(path.basename(filePath));
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=banner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"banner.js","sourceRoot":"","sources":["../../src/utils/banner.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"icon-composer.d.ts","sourceRoot":"","sources":["../../src/utils/icon-composer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"icon-composer.d.ts","sourceRoot":"","sources":["../../src/utils/icon-composer.ts"],"names":[],"mappings":"AA4CA,MAAM,WAAW,kBAAkB;IACjC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAS/D;AA6HD;;;GAGG;AACH,wBAAsB,wBAAwB,CAC5C,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,kBAAkB,CAAC,CA2H7B"}
|
|
@@ -128,8 +128,13 @@ export async function renderIconComposerFolder(iconFolderPath) {
|
|
|
128
128
|
}
|
|
129
129
|
const manifestRaw = fs.readFileSync(manifestPath, 'utf-8');
|
|
130
130
|
const manifest = JSON.parse(manifestRaw);
|
|
131
|
+
// Resolve fill: prefer top-level `fill`, fall back to first `fill-specializations` entry (light appearance)
|
|
132
|
+
const fill = manifest.fill ??
|
|
133
|
+
manifest['fill-specializations']?.find((s) => !s.appearance)?.value ??
|
|
134
|
+
manifest['fill-specializations']?.[0]?.value ??
|
|
135
|
+
{};
|
|
131
136
|
// Create background
|
|
132
|
-
const { buffer: backgroundBuffer, bgColor: extractedBgColor } = await createBackground(
|
|
137
|
+
const { buffer: backgroundBuffer, bgColor: extractedBgColor } = await createBackground(fill);
|
|
133
138
|
// Build composite operations for each layer
|
|
134
139
|
const compositeOps = [];
|
|
135
140
|
for (const group of manifest.groups) {
|
|
@@ -185,15 +190,32 @@ export async function renderIconComposerFolder(iconFolderPath) {
|
|
|
185
190
|
compositeOps.push({ input: layerBuffer, left, top });
|
|
186
191
|
}
|
|
187
192
|
}
|
|
188
|
-
// Compose final image
|
|
193
|
+
// Compose final image (with background)
|
|
189
194
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'iconwolf-compose-'));
|
|
190
195
|
const composedPath = path.join(tmpDir, 'composed-icon.png');
|
|
191
196
|
await sharp(backgroundBuffer)
|
|
192
197
|
.composite(compositeOps)
|
|
193
198
|
.png()
|
|
194
199
|
.toFile(composedPath);
|
|
200
|
+
// Compose foreground-only image (transparent background) for splash
|
|
201
|
+
const foregroundPath = path.join(tmpDir, 'foreground-icon.png');
|
|
202
|
+
const transparentBg = await sharp({
|
|
203
|
+
create: {
|
|
204
|
+
width: ICON_SIZE,
|
|
205
|
+
height: ICON_SIZE,
|
|
206
|
+
channels: 4,
|
|
207
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
.png()
|
|
211
|
+
.toBuffer();
|
|
212
|
+
await sharp(transparentBg)
|
|
213
|
+
.composite(compositeOps)
|
|
214
|
+
.png()
|
|
215
|
+
.toFile(foregroundPath);
|
|
195
216
|
return {
|
|
196
217
|
composedImagePath: composedPath,
|
|
218
|
+
foregroundImagePath: foregroundPath,
|
|
197
219
|
extractedBgColor,
|
|
198
220
|
};
|
|
199
221
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"icon-composer.js","sourceRoot":"","sources":["../../src/utils/icon-composer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,SAAS,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"icon-composer.js","sourceRoot":"","sources":["../../src/utils/icon-composer.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/generator.ts
CHANGED
|
@@ -11,10 +11,12 @@ import { generateStandardIcon } from './variants/standard.js';
|
|
|
11
11
|
import { generateFavicon } from './variants/favicon.js';
|
|
12
12
|
import { generateSplashIcon } from './variants/splash.js';
|
|
13
13
|
import { generateAndroidIcons } from './variants/android.js';
|
|
14
|
+
import { applyBanner, shouldApplyBanner } from './utils/banner.js';
|
|
14
15
|
import type { GeneratorOptions, GenerationResult } from './types.js';
|
|
15
16
|
|
|
16
17
|
interface ResolvedInput {
|
|
17
18
|
inputPath: string;
|
|
19
|
+
foregroundPath?: string;
|
|
18
20
|
cleanupPath?: string;
|
|
19
21
|
bgColor: string;
|
|
20
22
|
}
|
|
@@ -28,6 +30,7 @@ async function resolveInput(
|
|
|
28
30
|
if (!silent) logger.info(`Apple Icon Composer file: ${resolvedPath}`);
|
|
29
31
|
const result = await renderIconComposerFolder(resolvedPath);
|
|
30
32
|
const inputPath = result.composedImagePath;
|
|
33
|
+
const foregroundPath = result.foregroundImagePath;
|
|
31
34
|
const cleanupPath = path.dirname(result.composedImagePath);
|
|
32
35
|
|
|
33
36
|
if (bgColor === '#FFFFFF') {
|
|
@@ -35,7 +38,7 @@ async function resolveInput(
|
|
|
35
38
|
if (!silent) logger.info(`Extracted background color: ${bgColor}`);
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
return { inputPath, cleanupPath, bgColor };
|
|
41
|
+
return { inputPath, foregroundPath, cleanupPath, bgColor };
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
return { inputPath: resolvedPath, bgColor };
|
|
@@ -58,6 +61,7 @@ export async function generate(
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
let inputPath: string;
|
|
64
|
+
let foregroundPath: string | undefined;
|
|
61
65
|
let cleanupPath: string | undefined;
|
|
62
66
|
let splashPath: string | undefined;
|
|
63
67
|
let splashCleanupPath: string | undefined;
|
|
@@ -66,6 +70,7 @@ export async function generate(
|
|
|
66
70
|
// Resolve main input (Apple Icon Composer .icon folder or PNG)
|
|
67
71
|
const mainInput = await resolveInput(resolvedInput, bgColor, silent);
|
|
68
72
|
inputPath = mainInput.inputPath;
|
|
73
|
+
foregroundPath = mainInput.foregroundPath;
|
|
69
74
|
cleanupPath = mainInput.cleanupPath;
|
|
70
75
|
bgColor = mainInput.bgColor;
|
|
71
76
|
|
|
@@ -80,7 +85,7 @@ export async function generate(
|
|
|
80
85
|
bgColor,
|
|
81
86
|
silent,
|
|
82
87
|
);
|
|
83
|
-
splashPath = splashInput.inputPath;
|
|
88
|
+
splashPath = splashInput.foregroundPath ?? splashInput.inputPath;
|
|
84
89
|
splashCleanupPath = splashInput.cleanupPath;
|
|
85
90
|
}
|
|
86
91
|
|
|
@@ -141,13 +146,22 @@ export async function generate(
|
|
|
141
146
|
|
|
142
147
|
if (generateAll || variants.splash) {
|
|
143
148
|
const result = await generateSplashIcon(
|
|
144
|
-
splashPath || inputPath,
|
|
149
|
+
splashPath || foregroundPath || inputPath,
|
|
145
150
|
outputDir,
|
|
146
151
|
);
|
|
147
152
|
results.push(result);
|
|
148
153
|
if (!silent) logger.generated(result);
|
|
149
154
|
}
|
|
150
155
|
|
|
156
|
+
// Apply banner overlay if requested
|
|
157
|
+
if (options.banner) {
|
|
158
|
+
for (const result of results) {
|
|
159
|
+
if (shouldApplyBanner(result.filePath)) {
|
|
160
|
+
await applyBanner(result, options.banner);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
151
165
|
if (!silent) logger.summary(results);
|
|
152
166
|
|
|
153
167
|
return results;
|
package/src/index.ts
CHANGED
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
readCachedUpdateInfo,
|
|
9
9
|
refreshCacheInBackground,
|
|
10
10
|
} from './utils/update-notifier.js';
|
|
11
|
-
import type { GeneratorOptions } from './types.js';
|
|
11
|
+
import type { BannerPosition, GeneratorOptions } from './types.js';
|
|
12
12
|
|
|
13
|
-
const VERSION = '0.1
|
|
13
|
+
const VERSION = '0.2.1';
|
|
14
14
|
|
|
15
15
|
const program = new Command();
|
|
16
16
|
|
|
@@ -38,6 +38,16 @@ program
|
|
|
38
38
|
'Background color for Android adaptive icon',
|
|
39
39
|
'#FFFFFF',
|
|
40
40
|
)
|
|
41
|
+
.option(
|
|
42
|
+
'--banner <text>',
|
|
43
|
+
'Diagonal ribbon banner text (e.g. DEV, BETA, STAGING)',
|
|
44
|
+
)
|
|
45
|
+
.option('--banner-color <hex>', 'Ribbon color (default: auto from text)')
|
|
46
|
+
.option(
|
|
47
|
+
'--banner-position <pos>',
|
|
48
|
+
'Banner position: top-left, top-right, bottom-left, bottom-right',
|
|
49
|
+
'top-left',
|
|
50
|
+
)
|
|
41
51
|
.action(async (input: string, opts) => {
|
|
42
52
|
const updateInfo = readCachedUpdateInfo(VERSION);
|
|
43
53
|
refreshCacheInBackground().catch(() => {});
|
|
@@ -53,6 +63,13 @@ program
|
|
|
53
63
|
},
|
|
54
64
|
bgColor: opts.bgColor,
|
|
55
65
|
splashInputPath: opts.splashInput,
|
|
66
|
+
banner: opts.banner
|
|
67
|
+
? {
|
|
68
|
+
text: opts.banner,
|
|
69
|
+
color: opts.bannerColor,
|
|
70
|
+
position: opts.bannerPosition as BannerPosition,
|
|
71
|
+
}
|
|
72
|
+
: undefined,
|
|
56
73
|
};
|
|
57
74
|
|
|
58
75
|
try {
|
package/src/lib.ts
CHANGED
|
@@ -4,12 +4,19 @@ export { generateFavicon } from './variants/favicon.js';
|
|
|
4
4
|
export { generateSplashIcon } from './variants/splash.js';
|
|
5
5
|
export { generateAndroidIcons } from './variants/android.js';
|
|
6
6
|
export { validateSourceImage } from './utils/image.js';
|
|
7
|
+
export {
|
|
8
|
+
applyBanner,
|
|
9
|
+
createBannerSvg,
|
|
10
|
+
shouldApplyBanner,
|
|
11
|
+
} from './utils/banner.js';
|
|
7
12
|
export {
|
|
8
13
|
isIconComposerFolder,
|
|
9
14
|
renderIconComposerFolder,
|
|
10
15
|
} from './utils/icon-composer.js';
|
|
11
16
|
export { OUTPUT_FILES, resolveOutputPath } from './utils/paths.js';
|
|
12
17
|
export type {
|
|
18
|
+
BannerOptions,
|
|
19
|
+
BannerPosition,
|
|
13
20
|
VariantFlags,
|
|
14
21
|
GeneratorOptions,
|
|
15
22
|
GenerationResult,
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
export type BannerPosition =
|
|
2
|
+
| 'top-left'
|
|
3
|
+
| 'top-right'
|
|
4
|
+
| 'bottom-left'
|
|
5
|
+
| 'bottom-right';
|
|
6
|
+
|
|
7
|
+
export interface BannerOptions {
|
|
8
|
+
text: string;
|
|
9
|
+
color?: string;
|
|
10
|
+
position?: BannerPosition;
|
|
11
|
+
}
|
|
12
|
+
|
|
1
13
|
export interface VariantFlags {
|
|
2
14
|
android: boolean;
|
|
3
15
|
favicon: boolean;
|
|
@@ -11,6 +23,7 @@ export interface GeneratorOptions {
|
|
|
11
23
|
variants: VariantFlags;
|
|
12
24
|
bgColor: string;
|
|
13
25
|
splashInputPath?: string;
|
|
26
|
+
banner?: BannerOptions;
|
|
14
27
|
silent?: boolean;
|
|
15
28
|
}
|
|
16
29
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
|
+
import type {
|
|
5
|
+
BannerOptions,
|
|
6
|
+
BannerPosition,
|
|
7
|
+
GenerationResult,
|
|
8
|
+
} from '../types.js';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_COLORS: Record<string, string> = {
|
|
11
|
+
DEV: '#4CAF50',
|
|
12
|
+
BETA: '#FF9800',
|
|
13
|
+
STAGING: '#2196F3',
|
|
14
|
+
ALPHA: '#9C27B0',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const FALLBACK_COLOR = '#F44336';
|
|
18
|
+
|
|
19
|
+
function resolveColor(text: string, color?: string): string {
|
|
20
|
+
if (color) return color;
|
|
21
|
+
return DEFAULT_COLORS[text.toUpperCase()] ?? FALLBACK_COLOR;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createBannerSvg(
|
|
25
|
+
size: number,
|
|
26
|
+
text: string,
|
|
27
|
+
color: string,
|
|
28
|
+
position: BannerPosition,
|
|
29
|
+
): string {
|
|
30
|
+
const ribbonWidth = Math.round(size * 0.42);
|
|
31
|
+
const ribbonHeight = Math.round(size * 0.08);
|
|
32
|
+
const fontSize = Math.round(ribbonHeight * 0.6);
|
|
33
|
+
const halfDiag = ribbonWidth / 2;
|
|
34
|
+
|
|
35
|
+
// Center of the ribbon in each corner
|
|
36
|
+
let cx: number;
|
|
37
|
+
let cy: number;
|
|
38
|
+
let angle: number;
|
|
39
|
+
|
|
40
|
+
switch (position) {
|
|
41
|
+
case 'top-left':
|
|
42
|
+
cx = size * 0.2;
|
|
43
|
+
cy = size * 0.2;
|
|
44
|
+
angle = -45;
|
|
45
|
+
break;
|
|
46
|
+
case 'top-right':
|
|
47
|
+
cx = size * 0.8;
|
|
48
|
+
cy = size * 0.2;
|
|
49
|
+
angle = 45;
|
|
50
|
+
break;
|
|
51
|
+
case 'bottom-left':
|
|
52
|
+
cx = size * 0.2;
|
|
53
|
+
cy = size * 0.8;
|
|
54
|
+
angle = 45;
|
|
55
|
+
break;
|
|
56
|
+
case 'bottom-right':
|
|
57
|
+
cx = size * 0.8;
|
|
58
|
+
cy = size * 0.8;
|
|
59
|
+
angle = -45;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
|
64
|
+
<g transform="translate(${cx}, ${cy}) rotate(${angle})">
|
|
65
|
+
<rect x="${-halfDiag}" y="${-ribbonHeight / 2}" width="${ribbonWidth}" height="${ribbonHeight}" fill="${color}"/>
|
|
66
|
+
<text x="0" y="0" text-anchor="middle" dominant-baseline="central"
|
|
67
|
+
font-family="Arial, Helvetica, sans-serif" font-weight="bold"
|
|
68
|
+
font-size="${fontSize}" fill="white">${escapeXml(text)}</text>
|
|
69
|
+
</g>
|
|
70
|
+
</svg>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function escapeXml(s: string): string {
|
|
74
|
+
return s
|
|
75
|
+
.replace(/&/g, '&')
|
|
76
|
+
.replace(/</g, '<')
|
|
77
|
+
.replace(/>/g, '>')
|
|
78
|
+
.replace(/"/g, '"');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function applyBanner(
|
|
82
|
+
result: GenerationResult,
|
|
83
|
+
bannerOptions: BannerOptions,
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
const color = resolveColor(bannerOptions.text, bannerOptions.color);
|
|
86
|
+
const position = bannerOptions.position ?? 'top-left';
|
|
87
|
+
|
|
88
|
+
const meta = await sharp(result.filePath).metadata();
|
|
89
|
+
const size = meta.width ?? 1024;
|
|
90
|
+
|
|
91
|
+
const svg = createBannerSvg(size, bannerOptions.text, color, position);
|
|
92
|
+
const svgBuffer = Buffer.from(svg);
|
|
93
|
+
|
|
94
|
+
const tmpPath = result.filePath + '.tmp';
|
|
95
|
+
|
|
96
|
+
await sharp(result.filePath)
|
|
97
|
+
.composite([{ input: svgBuffer, top: 0, left: 0 }])
|
|
98
|
+
.png()
|
|
99
|
+
.toFile(tmpPath);
|
|
100
|
+
|
|
101
|
+
fs.renameSync(tmpPath, result.filePath);
|
|
102
|
+
|
|
103
|
+
const stat = fs.statSync(result.filePath);
|
|
104
|
+
result.size = stat.size;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Files that should not receive a banner overlay. */
|
|
108
|
+
const SKIP_FILES = new Set([
|
|
109
|
+
'favicon.png',
|
|
110
|
+
'android-icon-background.png',
|
|
111
|
+
'monochrome-icon.png',
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
export function shouldApplyBanner(filePath: string): boolean {
|
|
115
|
+
return !SKIP_FILES.has(path.basename(filePath));
|
|
116
|
+
}
|
|
@@ -30,14 +30,21 @@ interface IconComposerGroup {
|
|
|
30
30
|
translucency?: { enabled: boolean; value: number };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
interface IconComposerFillSpecialization {
|
|
34
|
+
appearance?: string;
|
|
35
|
+
value: IconComposerFill;
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
interface IconComposerManifest {
|
|
34
|
-
fill
|
|
39
|
+
fill?: IconComposerFill;
|
|
40
|
+
'fill-specializations'?: IconComposerFillSpecialization[];
|
|
35
41
|
groups: IconComposerGroup[];
|
|
36
42
|
'supported-platforms'?: unknown;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
export interface IconComposerResult {
|
|
40
46
|
composedImagePath: string;
|
|
47
|
+
foregroundImagePath: string;
|
|
41
48
|
extractedBgColor: string;
|
|
42
49
|
}
|
|
43
50
|
|
|
@@ -195,9 +202,16 @@ export async function renderIconComposerFolder(
|
|
|
195
202
|
const manifestRaw = fs.readFileSync(manifestPath, 'utf-8');
|
|
196
203
|
const manifest: IconComposerManifest = JSON.parse(manifestRaw);
|
|
197
204
|
|
|
205
|
+
// Resolve fill: prefer top-level `fill`, fall back to first `fill-specializations` entry (light appearance)
|
|
206
|
+
const fill: IconComposerFill =
|
|
207
|
+
manifest.fill ??
|
|
208
|
+
manifest['fill-specializations']?.find((s) => !s.appearance)?.value ??
|
|
209
|
+
manifest['fill-specializations']?.[0]?.value ??
|
|
210
|
+
{};
|
|
211
|
+
|
|
198
212
|
// Create background
|
|
199
213
|
const { buffer: backgroundBuffer, bgColor: extractedBgColor } =
|
|
200
|
-
await createBackground(
|
|
214
|
+
await createBackground(fill);
|
|
201
215
|
|
|
202
216
|
// Build composite operations for each layer
|
|
203
217
|
const compositeOps: sharp.OverlayOptions[] = [];
|
|
@@ -267,7 +281,7 @@ export async function renderIconComposerFolder(
|
|
|
267
281
|
}
|
|
268
282
|
}
|
|
269
283
|
|
|
270
|
-
// Compose final image
|
|
284
|
+
// Compose final image (with background)
|
|
271
285
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'iconwolf-compose-'));
|
|
272
286
|
const composedPath = path.join(tmpDir, 'composed-icon.png');
|
|
273
287
|
|
|
@@ -276,8 +290,28 @@ export async function renderIconComposerFolder(
|
|
|
276
290
|
.png()
|
|
277
291
|
.toFile(composedPath);
|
|
278
292
|
|
|
293
|
+
// Compose foreground-only image (transparent background) for splash
|
|
294
|
+
const foregroundPath = path.join(tmpDir, 'foreground-icon.png');
|
|
295
|
+
|
|
296
|
+
const transparentBg = await sharp({
|
|
297
|
+
create: {
|
|
298
|
+
width: ICON_SIZE,
|
|
299
|
+
height: ICON_SIZE,
|
|
300
|
+
channels: 4,
|
|
301
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
302
|
+
},
|
|
303
|
+
})
|
|
304
|
+
.png()
|
|
305
|
+
.toBuffer();
|
|
306
|
+
|
|
307
|
+
await sharp(transparentBg)
|
|
308
|
+
.composite(compositeOps)
|
|
309
|
+
.png()
|
|
310
|
+
.toFile(foregroundPath);
|
|
311
|
+
|
|
279
312
|
return {
|
|
280
313
|
composedImagePath: composedPath,
|
|
314
|
+
foregroundImagePath: foregroundPath,
|
|
281
315
|
extractedBgColor,
|
|
282
316
|
};
|
|
283
317
|
}
|
package/tests/cli.test.ts
CHANGED
|
@@ -40,6 +40,9 @@ describe('CLI end-to-end', () => {
|
|
|
40
40
|
expect(output).toContain('--icon');
|
|
41
41
|
expect(output).toContain('--splash-input');
|
|
42
42
|
expect(output).toContain('--bg-color');
|
|
43
|
+
expect(output).toContain('--banner');
|
|
44
|
+
expect(output).toContain('--banner-color');
|
|
45
|
+
expect(output).toContain('--banner-position');
|
|
43
46
|
});
|
|
44
47
|
|
|
45
48
|
it('generates icon.png via CLI with --icon flag', () => {
|
|
@@ -73,6 +76,14 @@ describe('CLI end-to-end', () => {
|
|
|
73
76
|
}).toThrow();
|
|
74
77
|
});
|
|
75
78
|
|
|
79
|
+
it('generates icon with --banner DEV flag', () => {
|
|
80
|
+
const outDir = path.join(tmpDir, 'cli-banner');
|
|
81
|
+
|
|
82
|
+
runCli(`${testPng} --icon --banner DEV -o ${outDir}`);
|
|
83
|
+
|
|
84
|
+
expect(fs.existsSync(path.join(outDir, 'icon.png'))).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
76
87
|
it('generates favicon when --favicon flag is used', () => {
|
|
77
88
|
const outDir = path.join(tmpDir, 'cli-favicon');
|
|
78
89
|
|
package/tests/generator.test.ts
CHANGED
|
@@ -347,6 +347,54 @@ describe('generate', () => {
|
|
|
347
347
|
expect(fs.existsSync(path.join(outDir, 'splash-icon.png'))).toBe(true);
|
|
348
348
|
});
|
|
349
349
|
|
|
350
|
+
it('applies banner to icon, adaptive-icon, and splash but not favicon', async () => {
|
|
351
|
+
const outDir = path.join(tmpDir, 'banner-test');
|
|
352
|
+
|
|
353
|
+
const results = await generate({
|
|
354
|
+
inputPath: testPng,
|
|
355
|
+
outputDir: outDir,
|
|
356
|
+
variants: { android: false, favicon: false, splash: false, icon: false },
|
|
357
|
+
bgColor: '#FFFFFF',
|
|
358
|
+
banner: { text: 'DEV' },
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// All 4 default files should exist
|
|
362
|
+
expect(fs.existsSync(path.join(outDir, OUTPUT_FILES.icon))).toBe(true);
|
|
363
|
+
expect(fs.existsSync(path.join(outDir, OUTPUT_FILES.favicon))).toBe(true);
|
|
364
|
+
expect(fs.existsSync(path.join(outDir, OUTPUT_FILES.splashIcon))).toBe(
|
|
365
|
+
true,
|
|
366
|
+
);
|
|
367
|
+
expect(
|
|
368
|
+
fs.existsSync(path.join(outDir, OUTPUT_FILES.androidForeground)),
|
|
369
|
+
).toBe(true);
|
|
370
|
+
expect(results.length).toBe(4);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('skips banner on background and monochrome android variants', async () => {
|
|
374
|
+
const outDir = path.join(tmpDir, 'banner-android');
|
|
375
|
+
|
|
376
|
+
const results = await generate({
|
|
377
|
+
inputPath: testPng,
|
|
378
|
+
outputDir: outDir,
|
|
379
|
+
variants: { android: true, favicon: false, splash: false, icon: false },
|
|
380
|
+
bgColor: '#FFFFFF',
|
|
381
|
+
banner: { text: 'STAGING', color: '#2196F3' },
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// adaptive-icon.png should exist (banner applied)
|
|
385
|
+
expect(
|
|
386
|
+
fs.existsSync(path.join(outDir, OUTPUT_FILES.androidForeground)),
|
|
387
|
+
).toBe(true);
|
|
388
|
+
// background and monochrome should exist (no banner)
|
|
389
|
+
expect(
|
|
390
|
+
fs.existsSync(path.join(outDir, OUTPUT_FILES.androidBackground)),
|
|
391
|
+
).toBe(true);
|
|
392
|
+
expect(
|
|
393
|
+
fs.existsSync(path.join(outDir, OUTPUT_FILES.androidMonochrome)),
|
|
394
|
+
).toBe(true);
|
|
395
|
+
expect(results.length).toBe(3);
|
|
396
|
+
});
|
|
397
|
+
|
|
350
398
|
it('throws on missing splash input file', async () => {
|
|
351
399
|
const outDir = path.join(tmpDir, 'splash-missing');
|
|
352
400
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import sharp from 'sharp';
|
|
3
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
createBannerSvg,
|
|
6
|
+
applyBanner,
|
|
7
|
+
shouldApplyBanner,
|
|
8
|
+
} from '../../src/utils/banner.js';
|
|
9
|
+
import { createTestPng, createTmpDir, cleanDir } from '../helpers.js';
|
|
10
|
+
|
|
11
|
+
let tmpDir: string;
|
|
12
|
+
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
tmpDir = createTmpDir();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterAll(() => {
|
|
18
|
+
cleanDir(tmpDir);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('createBannerSvg', () => {
|
|
22
|
+
it('generates valid SVG with correct dimensions', () => {
|
|
23
|
+
const svg = createBannerSvg(1024, 'DEV', '#4CAF50', 'top-left');
|
|
24
|
+
expect(svg).toContain('width="1024"');
|
|
25
|
+
expect(svg).toContain('height="1024"');
|
|
26
|
+
expect(svg).toContain('DEV');
|
|
27
|
+
expect(svg).toContain('#4CAF50');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders different positions', () => {
|
|
31
|
+
const positions = [
|
|
32
|
+
'top-left',
|
|
33
|
+
'top-right',
|
|
34
|
+
'bottom-left',
|
|
35
|
+
'bottom-right',
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
for (const pos of positions) {
|
|
39
|
+
const svg = createBannerSvg(1024, 'BETA', '#FF9800', pos);
|
|
40
|
+
expect(svg).toContain('BETA');
|
|
41
|
+
expect(svg).toContain('<svg');
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('escapes XML characters in text', () => {
|
|
46
|
+
const svg = createBannerSvg(1024, 'A&B<C>', '#FF0000', 'top-left');
|
|
47
|
+
expect(svg).toContain('A&B<C>');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('scales ribbon proportionally to icon size', () => {
|
|
51
|
+
const svg512 = createBannerSvg(512, 'DEV', '#4CAF50', 'top-left');
|
|
52
|
+
const svg1024 = createBannerSvg(1024, 'DEV', '#4CAF50', 'top-left');
|
|
53
|
+
expect(svg512).toContain('width="512"');
|
|
54
|
+
expect(svg1024).toContain('width="1024"');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('applyBanner', () => {
|
|
59
|
+
it('composites banner onto a PNG file', async () => {
|
|
60
|
+
const pngPath = await createTestPng(1024, 1024, tmpDir, 'banner-test.png');
|
|
61
|
+
const originalSize = fs.statSync(pngPath).size;
|
|
62
|
+
|
|
63
|
+
const result = {
|
|
64
|
+
filePath: pngPath,
|
|
65
|
+
width: 1024,
|
|
66
|
+
height: 1024,
|
|
67
|
+
size: originalSize,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
await applyBanner(result, { text: 'DEV' });
|
|
71
|
+
|
|
72
|
+
expect(fs.existsSync(pngPath)).toBe(true);
|
|
73
|
+
// The file should still be a valid PNG
|
|
74
|
+
const meta = await sharp(pngPath).metadata();
|
|
75
|
+
expect(meta.width).toBe(1024);
|
|
76
|
+
expect(meta.height).toBe(1024);
|
|
77
|
+
expect(meta.format).toBe('png');
|
|
78
|
+
// Size in result should be updated
|
|
79
|
+
expect(result.size).toBe(fs.statSync(pngPath).size);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('uses custom color when provided', async () => {
|
|
83
|
+
const pngPath = await createTestPng(
|
|
84
|
+
1024,
|
|
85
|
+
1024,
|
|
86
|
+
tmpDir,
|
|
87
|
+
'banner-custom-color.png',
|
|
88
|
+
);
|
|
89
|
+
const result = {
|
|
90
|
+
filePath: pngPath,
|
|
91
|
+
width: 1024,
|
|
92
|
+
height: 1024,
|
|
93
|
+
size: fs.statSync(pngPath).size,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
await applyBanner(result, { text: 'TEST', color: '#FF00FF' });
|
|
97
|
+
|
|
98
|
+
const meta = await sharp(pngPath).metadata();
|
|
99
|
+
expect(meta.width).toBe(1024);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('uses custom position', async () => {
|
|
103
|
+
const pngPath = await createTestPng(1024, 1024, tmpDir, 'banner-pos.png');
|
|
104
|
+
const result = {
|
|
105
|
+
filePath: pngPath,
|
|
106
|
+
width: 1024,
|
|
107
|
+
height: 1024,
|
|
108
|
+
size: fs.statSync(pngPath).size,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
await applyBanner(result, { text: 'DEV', position: 'bottom-right' });
|
|
112
|
+
|
|
113
|
+
const meta = await sharp(pngPath).metadata();
|
|
114
|
+
expect(meta.width).toBe(1024);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('shouldApplyBanner', () => {
|
|
119
|
+
it('returns true for icon.png', () => {
|
|
120
|
+
expect(shouldApplyBanner('/out/icon.png')).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('returns true for adaptive-icon.png', () => {
|
|
124
|
+
expect(shouldApplyBanner('/out/adaptive-icon.png')).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('returns true for splash-icon.png', () => {
|
|
128
|
+
expect(shouldApplyBanner('/out/splash-icon.png')).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('returns false for favicon.png', () => {
|
|
132
|
+
expect(shouldApplyBanner('/out/favicon.png')).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('returns false for android-icon-background.png', () => {
|
|
136
|
+
expect(shouldApplyBanner('/out/android-icon-background.png')).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('returns false for monochrome-icon.png', () => {
|
|
140
|
+
expect(shouldApplyBanner('/out/monochrome-icon.png')).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|