@stacksjs/image 0.75.151 → 0.75.153

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/social.js CHANGED
@@ -1 +1 @@
1
- import{mkdir}from"node:fs/promises";import process from"node:process";import{generateSocialCards,renderSocialCard}from"ts-images";import{loadFonts}from"./fonts";import{background,color,device,markPainter,projectFile,requireProjectFile,themed}from"./theme";const PRESET_SIZES={og:{width:1200,height:630},twitter:{width:1200,height:600},square:{width:1200,height:1200},portrait:{width:1200,height:1500}};export function socialCardName(path){const trimmed=path.replace(/^\/+|\/+$/g,"");return trimmed===""?"og":trimmed.replace(/[^a-z0-9]+/gi,"-").toLowerCase()}async function socialCardTheme(images,root){const social=themed(images,images.social),fonts=await loadFonts(images.fonts,root),mark=await markPainter(social.mark,root),presets=social.presets?.length?social.presets:["og"];return{social,presets,deviceOptions:device(social.device),shared:{titleFont:fonts.title,bodyFont:fonts.body,brand:social.brand,drawMark:mark?.draw,markAspect:mark?.aspect,markPlate:social.markPlate===!1?null:color(social.markPlate),surface:background(social.background,root),color:color(social.color),mutedColor:color(social.mutedColor),accent:color(social.accent),format:social.format??"jpeg",quality:social.quality,presets}}}export function textSizes(set,page){return{titleSize:page?.titleSize??set?.titleSize,eyebrowSize:page?.eyebrowSize??set?.eyebrowSize,subtitleSize:page?.subtitleSize??set?.subtitleSize}}function cardForeground(shot,deviceOptions,root,label){if(!shot)return;return{image:requireProjectFile(shot,root,label),radius:deviceOptions?.radius,borderColor:deviceOptions?.borderColor,shadow:deviceOptions?.shadow,scale:deviceOptions?.scale}}export async function generateSocialCardSet(images,root=process.cwd()){if(images.social?.enabled!==!0)return[];const{social,presets,deviceOptions,shared}=await socialCardTheme(images,root),outputDir=projectFile(social.outputDir??"public/social",root),publicPath=`/${(social.publicPath??"/social").replace(/^\/+|\/+$/g,"")}`;await mkdir(outputDir,{recursive:!0});const pages=social.pages?.length?social.pages:[{path:"/",title:social.brand??"Home"}],results=[];for(const page of pages){const name=socialCardName(page.path),shot=page.foreground??social.foreground,files=await generateSocialCards(outputDir,{...shared,...textSizes(social.text,page.text),name,title:page.title,eyebrow:page.eyebrow,subtitle:page.subtitle,foreground:cardForeground(shot,deviceOptions,root,`Product shot for ${page.path}`)}),urls=Object.fromEntries(Object.entries(files).map(([preset,file])=>[preset,`${publicPath}/${file.slice(file.lastIndexOf("/")+1)}`]));results.push({path:page.path,name,files,urls,width:PRESET_SIZES[presets[0]].width,height:PRESET_SIZES[presets[0]].height,title:page.title})}return results}export async function renderOnDemandSocialCard(images,card,root=process.cwd()){if(images.social?.enabled!==!0)return null;const{social,presets,deviceOptions,shared}=await socialCardTheme(images,root),preset=card.preset??presets[0],{width,height}=PRESET_SIZES[preset],format=shared.format,{presets:_presets,...cardOptions}=shared;return{bytes:await renderSocialCard({...cardOptions,...textSizes(social.text),width,height,title:card.title,eyebrow:card.eyebrow,subtitle:card.subtitle,foreground:cardForeground(card.foreground??social.foreground,deviceOptions,root,"On-demand product shot")}),contentType:format==="jpeg"?"image/jpeg":`image/${format}`,width,height}}export function socialMetaTags(card,siteUrl,format="jpeg"){const primary=`${siteUrl.replace(/\/+$/,"")}${card.urls.og??Object.values(card.urls)[0]}`,mimeType=format==="jpeg"?"image/jpeg":`image/${format}`;return[`<meta property="og:image" content="${primary}">`,`<meta property="og:image:type" content="${mimeType}">`,`<meta property="og:image:width" content="${card.width}">`,`<meta property="og:image:height" content="${card.height}">`,`<meta property="og:image:alt" content="${escapeAttribute(card.title)}">`,'<meta name="twitter:card" content="summary_large_image">',`<meta name="twitter:image" content="${primary}">`,`<meta name="twitter:image:alt" content="${escapeAttribute(card.title)}">`]}function escapeAttribute(value){return value.replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}
1
+ import{mkdir}from"node:fs/promises";import process from"node:process";import{generateSocialCards,renderSocialCard}from"ts-images";import{loadFonts}from"./fonts";import{background,color,device,markPainter,projectFile,requireProjectFile,themed}from"./theme";const PRESET_SIZES={og:{width:1200,height:630},twitter:{width:1200,height:600},square:{width:1200,height:1200},portrait:{width:1200,height:1500}};export function socialCardName(path){const trimmed=path.replace(/^\/+|\/+$/g,"");return trimmed===""?"og":trimmed.replace(/[^a-z0-9]+/gi,"-").toLowerCase()}async function socialCardTheme(images,root){const social=themed(images,images.social),fonts=await loadFonts(images.fonts,root),mark=await markPainter(social.mark,root,{colorScheme:social.markColorScheme}),presets=social.presets?.length?social.presets:["og"];return{social,presets,deviceOptions:device(social.device),shared:{titleFont:fonts.title,bodyFont:fonts.body,brand:social.brand,drawMark:mark?.draw,markAspect:mark?.aspect,markPlate:social.markPlate===!1?null:color(social.markPlate),surface:background(social.background,root),color:color(social.color),mutedColor:color(social.mutedColor),accent:color(social.accent),format:social.format??"jpeg",quality:social.quality,presets}}}export function textSizes(set,page){return{titleSize:page?.titleSize??set?.titleSize,eyebrowSize:page?.eyebrowSize??set?.eyebrowSize,subtitleSize:page?.subtitleSize??set?.subtitleSize}}function cardForeground(shot,deviceOptions,root,label){if(!shot)return;return{image:requireProjectFile(shot,root,label),radius:deviceOptions?.radius,borderColor:deviceOptions?.borderColor,shadow:deviceOptions?.shadow,scale:deviceOptions?.scale}}export async function generateSocialCardSet(images,root=process.cwd()){if(images.social?.enabled!==!0)return[];const{social,presets,deviceOptions,shared}=await socialCardTheme(images,root),outputDir=projectFile(social.outputDir??"public/social",root),publicPath=`/${(social.publicPath??"/social").replace(/^\/+|\/+$/g,"")}`;await mkdir(outputDir,{recursive:!0});const pages=social.pages?.length?social.pages:[{path:"/",title:social.brand??"Home"}],results=[];for(const page of pages){const name=socialCardName(page.path),shot=page.foreground??social.foreground,files=await generateSocialCards(outputDir,{...shared,...textSizes(social.text,page.text),name,title:page.title,eyebrow:page.eyebrow,subtitle:page.subtitle,foreground:cardForeground(shot,deviceOptions,root,`Product shot for ${page.path}`)}),urls=Object.fromEntries(Object.entries(files).map(([preset,file])=>[preset,`${publicPath}/${file.slice(file.lastIndexOf("/")+1)}`]));results.push({path:page.path,name,files,urls,width:PRESET_SIZES[presets[0]].width,height:PRESET_SIZES[presets[0]].height,title:page.title})}return results}export async function renderOnDemandSocialCard(images,card,root=process.cwd()){if(images.social?.enabled!==!0)return null;const{social,presets,deviceOptions,shared}=await socialCardTheme(images,root),preset=card.preset??presets[0],{width,height}=PRESET_SIZES[preset],format=shared.format,{presets:_presets,...cardOptions}=shared;return{bytes:await renderSocialCard({...cardOptions,...textSizes(social.text),width,height,title:card.title,eyebrow:card.eyebrow,subtitle:card.subtitle,foreground:cardForeground(card.foreground??social.foreground,deviceOptions,root,"On-demand product shot")}),contentType:format==="jpeg"?"image/jpeg":`image/${format}`,width,height}}export function socialMetaTags(card,siteUrl,format="jpeg"){const primary=`${siteUrl.replace(/\/+$/,"")}${card.urls.og??Object.values(card.urls)[0]}`,mimeType=format==="jpeg"?"image/jpeg":`image/${format}`;return[`<meta property="og:image" content="${primary}">`,`<meta property="og:image:type" content="${mimeType}">`,`<meta property="og:image:width" content="${card.width}">`,`<meta property="og:image:height" content="${card.height}">`,`<meta property="og:image:alt" content="${escapeAttribute(card.title)}">`,'<meta name="twitter:card" content="summary_large_image">',`<meta name="twitter:image" content="${primary}">`,`<meta name="twitter:image:alt" content="${escapeAttribute(card.title)}">`]}function escapeAttribute(value){return value.replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}
package/dist/theme.d.ts CHANGED
@@ -33,7 +33,7 @@ export declare function device(value: ImageDeviceConfig | undefined): {
33
33
  scale?: number
34
34
  shadow?: { blur?: number, offsetX?: number, offsetY?: number, spread?: number, color?: RGBA }
35
35
  } | undefined;
36
- export declare function markPainter(path: string | undefined, root?: string): Promise<MarkPainter | undefined>;
36
+ export declare function markPainter(path: string | undefined, root?: string, options?: MarkPainterOptions): Promise<MarkPainter | undefined>;
37
37
  /**
38
38
  * Crop the transparent margin off a mark, keeping its own proportions.
39
39
  *
@@ -59,6 +59,9 @@ export declare interface MarkPainter {
59
59
  draw: (canvas: ImageData, box: { x: number, y: number, size: number, width?: number }) => void
60
60
  aspect: number
61
61
  }
62
+ export declare interface MarkPainterOptions {
63
+ colorScheme?: 'light' | 'dark'
64
+ }
62
65
  /** The palette keys a generator inherits from the top level of the config. */
63
66
  export declare interface ImageTheme {
64
67
  background?: ImageBackgroundConfig
package/dist/theme.js CHANGED
@@ -1,2 +1,2 @@
1
- import{existsSync}from"node:fs";import{readFile}from"node:fs/promises";import{isAbsolute,resolve}from"node:path";import process from"node:process";import{crop,decode,drawImage,parseColor}from"ts-images";export function projectFile(path,root=process.cwd()){return isAbsolute(path)?path:resolve(root,path)}export function requireProjectFile(path,root,describe){const resolved=projectFile(path,root);if(!existsSync(resolved))throw Error(`[image] ${describe} not found: ${path}
2
- Looked in ${resolved}. Capture it before generating - \`buddy generate:images\` frames existing captures, it does not take them.`);return resolved}export function color(value){return value===void 0?void 0:parseColor(value)}export function background(value,root=process.cwd()){if(!value)return;return{color:color(value.color),gradient:value.gradient&&{angle:value.gradient.angle,stops:value.gradient.stops.map((stop)=>({offset:stop.offset,color:parseColor(stop.color)}))},glows:value.glows?.map((glow)=>({...glow,color:parseColor(glow.color)})),image:value.image?projectFile(value.image,root):void 0}}export function device(value){if(!value)return;return{radius:value.radius,scale:value.scale,borderColor:color(value.borderColor),shadow:value.shadow===!1?void 0:{...value.shadow??{},color:color(value.shadow?.color)}}}export async function markPainter(path,root=process.cwd()){if(!path)return;const mark=trimTransparentEdges(await decode(new Uint8Array(await readFile(projectFile(path,root))))),aspect=mark.height>0?mark.width/mark.height:1;return{aspect,draw:(canvas,box)=>{drawImage(canvas,mark,{x:box.x,y:box.y,width:box.width??box.size*aspect,height:box.size,fit:"contain"})}}}export function trimTransparentEdges(image,alphaThreshold=8){let{width:left,height:top}=image,right=-1,bottom=-1;for(let y=0;y<image.height;y++)for(let x=0;x<image.width;x++)if((image.data[(y*image.width+x)*4+3]??0)>alphaThreshold){if(x<left)left=x;if(x>right)right=x;if(y<top)top=y;if(y>bottom)bottom=y}if(right<0||left===0&&top===0&&right===image.width-1&&bottom===image.height-1)return image;return crop(image,{left,top,width:right-left+1,height:bottom-top+1})}export function themed(images,section){return{...section??{},background:section?.background??images.background,color:section?.color??images.color,mutedColor:section?.mutedColor??images.mutedColor,accent:section?.accent??images.accent,device:section?.device??images.device,brand:section?.brand??images.brand,mark:section?.mark??images.mark,markPlate:section?.markPlate!==void 0?section.markPlate:images.markPlate}}
1
+ import{existsSync}from"node:fs";import{readFile}from"node:fs/promises";import{isAbsolute,resolve}from"node:path";import process from"node:process";import{parseSVG,rasterize}from"@stacksjs/ts-svg";import{createImageData,crop,decode,drawImage,parseColor}from"ts-images";export function projectFile(path,root=process.cwd()){return isAbsolute(path)?path:resolve(root,path)}export function requireProjectFile(path,root,describe){const resolved=projectFile(path,root);if(!existsSync(resolved))throw Error(`[image] ${describe} not found: ${path}
2
+ Looked in ${resolved}. Capture it before generating - \`buddy generate:images\` frames existing captures, it does not take them.`);return resolved}export function color(value){return value===void 0?void 0:parseColor(value)}export function background(value,root=process.cwd()){if(!value)return;return{color:color(value.color),gradient:value.gradient&&{angle:value.gradient.angle,stops:value.gradient.stops.map((stop)=>({offset:stop.offset,color:parseColor(stop.color)}))},glows:value.glows?.map((glow)=>({...glow,color:parseColor(glow.color)})),image:value.image?projectFile(value.image,root):void 0}}export function device(value){if(!value)return;return{radius:value.radius,scale:value.scale,borderColor:color(value.borderColor),shadow:value.shadow===!1?void 0:{...value.shadow??{},color:color(value.shadow?.color)}}}function svgMark(source,colorScheme){const root=parseSVG(source,{colorScheme}),draw=(height)=>{const frame=rasterize(root,{height:Math.max(1,Math.round(height))}),image=createImageData(frame.width,frame.height);image.data.set(frame.data);return trimTransparentEdges(image)},SAMPLE=512,sample=draw(SAMPLE),fill=sample.height>0?sample.height/SAMPLE:1;return{render:(height)=>draw(height/fill),aspect:sample.height>0?sample.width/sample.height:1}}export async function markPainter(path,root=process.cwd(),options={}){if(!path)return;const file=projectFile(path,root);if(/\.svg$/i.test(file)){const{render,aspect}=svgMark(await readFile(file,"utf8"),options.colorScheme??"light");return{aspect,draw:(canvas,box)=>{const mark=render(box.size);drawImage(canvas,mark,{x:box.x,y:box.y,width:box.width??box.size*aspect,height:box.size,fit:"contain"})}}}const mark=trimTransparentEdges(await decode(new Uint8Array(await readFile(file)))),aspect=mark.height>0?mark.width/mark.height:1;return{aspect,draw:(canvas,box)=>{drawImage(canvas,mark,{x:box.x,y:box.y,width:box.width??box.size*aspect,height:box.size,fit:"contain"})}}}export function trimTransparentEdges(image,alphaThreshold=8){let{width:left,height:top}=image,right=-1,bottom=-1;for(let y=0;y<image.height;y++)for(let x=0;x<image.width;x++)if((image.data[(y*image.width+x)*4+3]??0)>alphaThreshold){if(x<left)left=x;if(x>right)right=x;if(y<top)top=y;if(y>bottom)bottom=y}if(right<0||left===0&&top===0&&right===image.width-1&&bottom===image.height-1)return image;return crop(image,{left,top,width:right-left+1,height:bottom-top+1})}export function themed(images,section){return{...section??{},background:section?.background??images.background,color:section?.color??images.color,mutedColor:section?.mutedColor??images.mutedColor,accent:section?.accent??images.accent,device:section?.device??images.device,brand:section?.brand??images.brand,mark:section?.mark??images.mark,markPlate:section?.markPlate!==void 0?section.markPlate:images.markPlate}}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/image",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.151",
5
+ "version": "0.75.153",
6
6
  "description": "Native responsive image delivery for Stacks.",
7
7
  "author": "Chris Breuer",
8
8
  "license": "MIT",
@@ -31,7 +31,8 @@
31
31
  "prepublishOnly": "bun run build"
32
32
  },
33
33
  "dependencies": {
34
- "@stacksjs/types": "0.75.151",
34
+ "@stacksjs/ts-svg": "^0.1.5",
35
+ "@stacksjs/types": "0.75.153",
35
36
  "ts-images": "^0.2.23"
36
37
  },
37
38
  "devDependencies": {