-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #159 from dcSpark/fix/create-new-icons
Using BF API to create banner and icons.
- Loading branch information
Showing
78 changed files
with
326 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { Image, TextLayout } from "https://deno.land/x/imagescript@1.2.15/mod.ts"; | ||
import { join, dirname, fromFileUrl } from "https://deno.land/std@0.210.0/path/mod.ts"; | ||
import { existsSync } from "https://deno.land/std@0.210.0/fs/mod.ts"; | ||
|
||
const __dirname = dirname(fromFileUrl(import.meta.url)); | ||
const toolsDir = join(__dirname, '..', 'tools'); | ||
const fontPath = join(__dirname, 'Lato-Bold.ttf'); | ||
|
||
async function addTextToImage(inputPath: string, outputPath: string, toolName: string): Promise<void> { | ||
try { | ||
// Load the background image | ||
const image = await Image.decode(await Deno.readFile(inputPath)); | ||
const width = image.width; | ||
const height = image.height; | ||
|
||
// Calculate font size (start with 15% of height) | ||
const fontSize = Math.floor(height * 0.15); | ||
console.log(`Image dimensions: ${width}x${height}, fontSize: ${fontSize}`); | ||
|
||
// Load font | ||
const font = await Deno.readFile(fontPath); | ||
|
||
// Create text layout | ||
const layout = new TextLayout({ | ||
maxWidth: Math.floor(width * 0.9), | ||
maxHeight: Math.floor(height * 0.9), | ||
wrapStyle: "word", | ||
align: "center" | ||
}); | ||
|
||
// Create shadow text | ||
const shadowText = Image.renderText(font, fontSize, toolName, 0x000000FF, layout); | ||
|
||
// Calculate bottom right position with padding | ||
const padding = Math.floor(height * 0.05); // 5% of height as padding | ||
let x = Math.floor(width - shadowText.width - padding); | ||
let y = Math.floor(height - shadowText.height - padding); | ||
console.log(`Text position: (${x},${y})`); | ||
|
||
// Ensure we don't go beyond image boundaries | ||
if (x < 0) x = padding; | ||
if (y < 0) y = padding; | ||
|
||
// Draw shadow with multiple offsets for thickness | ||
for (let offsetX = 1; offsetX <= 4; offsetX++) { | ||
for (let offsetY = 1; offsetY <= 4; offsetY++) { | ||
await image.composite(shadowText, x + offsetX, y + offsetY); | ||
} | ||
} | ||
|
||
// Create and draw main text | ||
const mainText = Image.renderText(font, fontSize, toolName, 0xFFFFFFFF, layout); | ||
await image.composite(mainText, x, y); | ||
|
||
// Save the result | ||
await Deno.writeFile(outputPath, await image.encode()); | ||
} catch (error) { | ||
console.error(`Error processing ${inputPath}:`, error); | ||
throw error; | ||
} | ||
} | ||
|
||
async function readToolMetadata(toolPath: string) { | ||
const metadataPath = join(toolPath, 'metadata.json'); | ||
const metadata = JSON.parse(await Deno.readTextFile(metadataPath)); | ||
return metadata; | ||
} | ||
|
||
async function main() { | ||
// Get all tool directories | ||
const tools = Array.from(Deno.readDirSync(toolsDir)) | ||
.filter(item => item.isDirectory) | ||
.map(item => item.name); | ||
|
||
for (const tool of tools) { | ||
const toolPath = join(toolsDir, tool); | ||
const backgroundPath = join(toolPath, 'banner_background.png'); | ||
const bannerPath = join(toolPath, 'banner.png'); | ||
|
||
// Skip if background doesn't exist | ||
if (!existsSync(backgroundPath)) { | ||
console.log(`Skipping ${tool}: no banner_background.png found`); | ||
continue; | ||
} | ||
|
||
try { | ||
console.log(`Processing ${tool}...`); | ||
const metadata = await readToolMetadata(toolPath); | ||
await addTextToImage(backgroundPath, bannerPath, metadata.name); | ||
console.log(`Successfully generated banner.png for ${tool}`); | ||
} catch (error) { | ||
console.error(`Error processing tool ${tool}:`, error); | ||
} | ||
} | ||
} | ||
|
||
// Run if called directly | ||
if (import.meta.main) { | ||
main().catch(error => { | ||
console.error('Fatal error:', error); | ||
Deno.exit(1); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
import { config as dotenvConfig } from "https://deno.land/x/dotenv@v3.2.2/mod.ts"; | ||
import { join, dirname, fromFileUrl } from "https://deno.land/std@0.210.0/path/mod.ts"; | ||
import { existsSync } from "https://deno.land/std@0.210.0/fs/mod.ts"; | ||
import { createCanvas, loadImage } from "https://deno.land/x/canvas/mod.ts"; | ||
import axios from "npm:axios@1.6.2"; | ||
|
||
// Load environment variables | ||
dotenvConfig(); | ||
|
||
const BANNER_WIDTH = 1280; | ||
const BANNER_HEIGHT = 640; | ||
const ICON_SIZE = 256; | ||
const BFL_API_URL = 'https://api.us1.bfl.ai/v1'; | ||
const BFL_API_KEY = Deno.env.get("BFL_API_KEY"); | ||
|
||
interface ToolMetadata { | ||
name: string; | ||
description: string; | ||
} | ||
|
||
interface GenerateImageOptions { | ||
prompt: string; | ||
negative_prompt?: string; | ||
width?: number; | ||
height?: number; | ||
num_outputs?: number; | ||
guidance_scale?: number; | ||
seed?: number; | ||
} | ||
|
||
interface BFLResponse { | ||
id: string; | ||
status: string; | ||
result?: { | ||
sample: string; | ||
}; | ||
} | ||
|
||
async function readToolMetadata(toolPath: string): Promise<ToolMetadata> { | ||
const metadataPath = join(toolPath, 'metadata.json'); | ||
const metadata = JSON.parse(await Deno.readTextFile(metadataPath)); | ||
return metadata; | ||
} | ||
|
||
async function waitForImageGeneration(requestId: string): Promise<string> { | ||
while (true) { | ||
const response = await axios.get<BFLResponse>(`${BFL_API_URL}/get_result?id=${requestId}`, { | ||
headers: { | ||
'accept': 'application/json', | ||
'x-key': BFL_API_KEY | ||
} | ||
}); | ||
|
||
if (response.data.status === 'Ready' && response.data.result) { | ||
return response.data.result.sample; | ||
} | ||
|
||
if (response.data.status === 'Failed') { | ||
throw new Error('Image generation failed'); | ||
} | ||
|
||
// Wait before polling again | ||
await new Promise(resolve => setTimeout(resolve, 500)); | ||
} | ||
} | ||
|
||
async function generateImage(options: GenerateImageOptions): Promise<string> { | ||
const defaultOptions = { | ||
width: 1024, | ||
height: 1024, | ||
// prompt_upsampling: true, | ||
}; | ||
|
||
if (!options.prompt) { | ||
throw new Error('Prompt is required'); | ||
} | ||
|
||
const mergedOptions = { ...defaultOptions, ...options }; | ||
|
||
try { | ||
// Initial request to start generation | ||
const response = await axios.post<BFLResponse>( | ||
`${BFL_API_URL}/flux-pro-1.1`, | ||
{ | ||
prompt: mergedOptions.prompt, | ||
width: mergedOptions.width, | ||
height: mergedOptions.height | ||
}, | ||
{ | ||
headers: { | ||
'accept': 'application/json', | ||
'x-key': BFL_API_KEY, | ||
'Content-Type': 'application/json' | ||
} | ||
} | ||
); | ||
|
||
// Poll for results | ||
return await waitForImageGeneration(response.data.id); | ||
} catch (error) { | ||
console.error('Error generating image:', error); | ||
throw error; | ||
} | ||
} | ||
|
||
async function copyPlaceholderImages(scriptDir: string, toolPath: string, missingFiles: string[]): Promise<void> { | ||
console.log("Using placeholder images for:", missingFiles.join(", ")); | ||
for (const file of missingFiles) { | ||
const sourcePath = join(scriptDir, file); | ||
const destPath = join(toolPath, file); | ||
if (existsSync(sourcePath)) { | ||
await Deno.copyFile(sourcePath, destPath); | ||
} | ||
} | ||
} | ||
|
||
async function generateToolImages(toolPath: string, metadata: ToolMetadata): Promise<void> { | ||
const bannerPath = join(toolPath, 'banner_background.png'); | ||
const iconPath = join(toolPath, 'icon.png'); | ||
const missingFiles: string[] = []; | ||
|
||
if (!existsSync(bannerPath)) missingFiles.push('banner_background.png'); | ||
if (!existsSync(iconPath)) missingFiles.push('icon.png'); | ||
|
||
if (!missingFiles.length) { | ||
console.log("All images already exist, skipping generation"); | ||
return; | ||
} | ||
|
||
console.log(`Generating missing images: ${missingFiles.join(', ')}`); | ||
|
||
try { | ||
if (missingFiles.includes('banner_background.png')) { | ||
const bannerPrompt = | ||
`Create a professional banner image for a software tool named '${metadata.name}'. ` + | ||
`The tool description is: ${metadata.description}. Use a modern, minimalist style with subtle tech elements. ` + | ||
"Make it suitable for a developer tool interface. Use a cohesive color scheme, dark colors, allowing for white text to be readable on the image. " + | ||
"and include some abstract geometric elements that represent the tool's function." + | ||
"Do not include any text, just create a beautiful image. And I repeat, do not include any text."; | ||
|
||
console.log(`Generating banner for ${metadata.name}...`); | ||
const bannerUrl = await generateImage({ | ||
prompt: bannerPrompt, | ||
width: 1280, | ||
height: 736, | ||
num_outputs: 1 | ||
}); | ||
|
||
const response = await axios.get(bannerUrl, { responseType: 'arraybuffer' }); | ||
await Deno.writeFile(join(toolPath, 'banner_background.png'), new Uint8Array(response.data)); | ||
} | ||
|
||
if (missingFiles.includes('icon.png')) { | ||
const iconPrompt = | ||
`Create a simple, memorable icon for '${metadata.name}'. The icon should represent ` + | ||
`${metadata.description}. Use a minimal design with clear shapes and limited colors. ` + | ||
"Make it recognizable at small sizes and suitable for a developer tool. " + | ||
"The icon should work well as an app icon or menu item."; | ||
|
||
console.log(`Generating icon for ${metadata.name}...`); | ||
const iconUrl = await generateImage({ | ||
prompt: iconPrompt, | ||
width: 256, | ||
height: 256, | ||
num_outputs: 1 | ||
}); | ||
|
||
const response = await axios.get(iconUrl, { responseType: 'arraybuffer' }); | ||
await Deno.writeFile(join(toolPath, 'icon.png'), new Uint8Array(response.data)); | ||
} | ||
|
||
console.log("Successfully generated missing images"); | ||
} catch (error) { | ||
console.error('Error generating images:', error); | ||
throw error; | ||
} | ||
} | ||
|
||
async function main() { | ||
if (!BFL_API_KEY) { | ||
console.error('Please set BFL_API_KEY environment variable'); | ||
Deno.exit(1); | ||
} | ||
|
||
const __dirname = dirname(fromFileUrl(import.meta.url)); | ||
const toolsDir = join(__dirname, '..', 'tools'); | ||
const scriptDir = __dirname; | ||
|
||
// Get all tool directories | ||
const tools = Array.from(Deno.readDirSync(toolsDir)) | ||
.filter(item => item.isDirectory) | ||
.map(item => item.name); | ||
|
||
for (const tool of tools) { | ||
const toolPath = join(toolsDir, tool); | ||
console.log(`\nProcessing tool: ${tool}`); | ||
|
||
try { | ||
const metadata = await readToolMetadata(toolPath); | ||
await generateToolImages(toolPath, metadata); | ||
} catch (error) { | ||
console.error(`Error processing tool ${tool}:`, error); | ||
// Copy placeholder images if available | ||
const missingFiles = []; | ||
if (!existsSync(join(toolPath, 'banner_background.png'))) missingFiles.push('banner_background.png'); | ||
if (!existsSync(join(toolPath, 'icon.png'))) missingFiles.push('icon.png'); | ||
if (missingFiles.length) { | ||
await copyPlaceholderImages(scriptDir, toolPath, missingFiles); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Run if called directly | ||
if (import.meta.main) { | ||
main().catch(error => { | ||
console.error('Fatal error:', error); | ||
Deno.exit(1); | ||
}); | ||
} | ||
|
||
export { generateImage, GenerateImageOptions }; |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.