PluginProbe
Extendify / 3.1.6
Extendify v3.1.6
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.1.6, at src/Shared/lib/ai-label.js

123 lines 4.0 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 drawPill = (ctx, x, y, fontSize) => {
37 const label = aiLabelText();
38 const { width, height } = pillSize(ctx, fontSize, label);
39 const lineWidth = Math.max(1, fontSize / 12);
40 pillPath(ctx, x, y, width, height);
41 ctx.fillStyle = '#1a1a1a';
42 ctx.fill();
43 // A centered stroke would eat half the border out of the fill box.
44 pillPath(
45 ctx,
46 x - lineWidth / 2,
47 y - lineWidth / 2,
48 width + lineWidth,
49 height + lineWidth,
50 );
51 ctx.lineWidth = lineWidth;
52 ctx.strokeStyle = '#fff';
53 ctx.stroke();
54 ctx.fillStyle = '#fff';
55 ctx.textAlign = 'center';
56 const metrics = ctx.measureText(label);
57 const { actualBoundingBoxAscent: ascent, actualBoundingBoxDescent: descent } =
58 metrics;
59 if (ascent === undefined || descent === undefined) {
60 ctx.textBaseline = 'middle';
61 ctx.fillText(label, x + width / 2, y + height / 2);
62 return;
63 }
64 // The 'middle' baseline centers the em box, which sits the glyphs too high.
65 ctx.textBaseline = 'alphabetic';
66 ctx.fillText(label, x + width / 2, y + height / 2 + (ascent - descent) / 2);
67 };
68
69 export const stampAiLabel = (ctx, width, height) => {
70 const fontSize = Math.max(12, Math.round(Math.min(width, height) * 0.02));
71 const pill = pillSize(ctx, fontSize, aiLabelText());
72 // The 16px is to the border's outer edge, which sits past the fill.
73 const inset = 16 + Math.max(1, fontSize / 12);
74 drawPill(
75 ctx,
76 width - pill.width - inset,
77 height - pill.height - inset,
78 fontSize,
79 );
80 };
81
82 // GD on the server has no font for the localized label; ship it a rendered PNG.
83 export const renderAiLabelPill = async () => {
84 const canvas = document.createElement('canvas');
85 const ctx = canvas.getContext('2d');
86 if (!ctx) return null;
87 const fontSize = 48;
88 // Below the line width, the border clips at the bitmap edge.
89 const margin = Math.ceil(fontSize / 12);
90 const { width, height } = pillSize(ctx, fontSize, aiLabelText());
91 canvas.width = Math.ceil(width) + margin * 2;
92 canvas.height = Math.ceil(height) + margin * 2;
93 drawPill(ctx, margin, margin, fontSize);
94 return await new Promise((resolve) => {
95 canvas.toBlob((blob) => resolve(blob), 'image/png');
96 });
97 };
98
99 export const stampImageBlob = async (imageUrl) => {
100 try {
101 const image = new Image();
102 image.crossOrigin = 'anonymous';
103 const loaded = new Promise((resolve, reject) => {
104 image.onload = () => resolve(image);
105 image.onerror = reject;
106 });
107 image.src = imageUrl;
108 await loaded;
109 const canvas = document.createElement('canvas');
110 canvas.width = image.naturalWidth || image.width;
111 canvas.height = image.naturalHeight || image.height;
112 const ctx = canvas.getContext('2d');
113 if (!ctx) return null;
114 ctx.drawImage(image, 0, 0);
115 stampAiLabel(ctx, canvas.width, canvas.height);
116 return await new Promise((resolve) => {
117 canvas.toBlob((blob) => resolve(blob), 'image/jpeg');
118 });
119 } catch {
120 return null;
121 }
122 };
123