PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / Shared / lib / ai-label.js

ai-label.js in Extendify 3.2.1, at src/Shared/lib/ai-label.js

126 lines 4.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 const siteLocale = () => window.extSharedData?.wpLanguage?.replace('_', '-');
2
3 const RTL_LANGUAGES = new Set(['ar', 'ary', 'fa', 'he', 'ur']);
4
5 // Already all-caps in the site's locale; casing it here would break Turkish and Greek.
6 const aiLabelText = () => window.extSharedData?.aiImageLabel ?? 'AI GENERATED';
7
8 const setPillFont = (ctx, fontSize) => {
9 ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
10 // Older engines lack tracking; the label still reads without it.
11 if ('letterSpacing' in ctx) ctx.letterSpacing = `${fontSize * 0.06}px`;
12 // A Hebrew label with a Latin run inside orders wrongly under an ltr base.
13 const language = (siteLocale() || '').split('-')[0].toLowerCase();
14 ctx.direction = RTL_LANGUAGES.has(language) ? 'rtl' : 'ltr';
15 };
16
17 const pillSize = (ctx, fontSize, label) => {
18 setPillFont(ctx, fontSize);
19 return {
20 width: ctx.measureText(label).width + fontSize * 1.2,
21 height: fontSize * 1.7,
22 };
23 };
24
25 const pillPath = (ctx, x, y, width, height) => {
26 const radius = height / 2;
27 ctx.beginPath();
28 ctx.moveTo(x + radius, y);
29 ctx.arcTo(x + width, y, x + width, y + height, radius);
30 ctx.arcTo(x + width, y + height, x, y + height, radius);
31 ctx.arcTo(x, y + height, x, y, radius);
32 ctx.arcTo(x, y, x + width, y, radius);
33 ctx.closePath();
34 };
35
36 const pillLineWidth = (fontSize) => Math.max(1, fontSize / 12);
37
38 const drawPill = (ctx, x, y, fontSize) => {
39 const label = aiLabelText();
40 const { width, height } = pillSize(ctx, fontSize, label);
41 const lineWidth = pillLineWidth(fontSize);
42 pillPath(ctx, x, y, width, height);
43 ctx.fillStyle = '#1a1a1a';
44 ctx.fill();
45 // A centered stroke would eat half the border out of the fill box.
46 pillPath(
47 ctx,
48 x - lineWidth / 2,
49 y - lineWidth / 2,
50 width + lineWidth,
51 height + lineWidth,
52 );
53 ctx.lineWidth = lineWidth;
54 ctx.strokeStyle = '#fff';
55 ctx.stroke();
56 ctx.fillStyle = '#fff';
57 ctx.textAlign = 'center';
58 const metrics = ctx.measureText(label);
59 const { actualBoundingBoxAscent: ascent, actualBoundingBoxDescent: descent } =
60 metrics;
61 if (ascent === undefined || descent === undefined) {
62 ctx.textBaseline = 'middle';
63 ctx.fillText(label, x + width / 2, y + height / 2);
64 return;
65 }
66 // The 'middle' baseline centers the em box, which sits the glyphs too high.
67 ctx.textBaseline = 'alphabetic';
68 ctx.fillText(label, x + width / 2, y + height / 2 + (ascent - descent) / 2);
69 };
70
71 // Beyond 87.5% our narrowest crop, a 3:4 cover, eats the label.
72 const PILL_RIGHT_EDGE = 0.85;
73
74 export const stampAiLabel = (ctx, width, height) => {
75 const fontSize = Math.max(12, Math.round(Math.min(width, height) * 0.02));
76 const pill = pillSize(ctx, fontSize, aiLabelText());
77 drawPill(
78 ctx,
79 width * PILL_RIGHT_EDGE - pill.width - pillLineWidth(fontSize),
80 (height - pill.height) / 2,
81 fontSize,
82 );
83 };
84
85 // GD on the server has no font for the localized label; ship it a rendered PNG.
86 export const renderAiLabelPill = async () => {
87 const canvas = document.createElement('canvas');
88 const ctx = canvas.getContext('2d');
89 if (!ctx) return null;
90 const fontSize = 48;
91 // Below the line width, the border clips at the bitmap edge.
92 const margin = Math.ceil(fontSize / 12);
93 const { width, height } = pillSize(ctx, fontSize, aiLabelText());
94 canvas.width = Math.ceil(width) + margin * 2;
95 canvas.height = Math.ceil(height) + margin * 2;
96 drawPill(ctx, margin, margin, fontSize);
97 return await new Promise((resolve) => {
98 canvas.toBlob((blob) => resolve(blob), 'image/png');
99 });
100 };
101
102 export const stampImageBlob = async (imageUrl) => {
103 try {
104 const image = new Image();
105 image.crossOrigin = 'anonymous';
106 const loaded = new Promise((resolve, reject) => {
107 image.onload = () => resolve(image);
108 image.onerror = reject;
109 });
110 image.src = imageUrl;
111 await loaded;
112 const canvas = document.createElement('canvas');
113 canvas.width = image.naturalWidth || image.width;
114 canvas.height = image.naturalHeight || image.height;
115 const ctx = canvas.getContext('2d');
116 if (!ctx) return null;
117 ctx.drawImage(image, 0, 0);
118 stampAiLabel(ctx, canvas.width, canvas.height);
119 return await new Promise((resolve) => {
120 canvas.toBlob((blob) => resolve(blob), 'image/jpeg');
121 });
122 } catch {
123 return null;
124 }
125 };
126