auto-svg / app.js
Mbonea's picture
temp dir
5f0b5f5
const express = require("express");
const cors = require("cors");
const opentype = require("opentype.js");
const makerjs = require("makerjs");
const wawoff2 = require("wawoff2");
const xml2js = require("xml2js");
const axios = require("axios");
const fs = require("fs").promises;
const os = require("os");
const path = require("path");
const getImageOutline = require("image-outline");
const Tesseract = require("tesseract.js");
const app = express();
const FormData = require("form-data");
class Kolors {
constructor() {
this.commonHeaders = {
accept: "*/*",
"accept-language": "en-US,en;q=0.9",
priority: "u=1, i",
referer: "https://kwai-kolors-kolors.hf.space/?__theme=dark",
origin: "https://kwai-kolors-kolors.hf.space",
};
}
async processRequest(method, url, headers = {}, data = null, files = null) {
const config = {
method,
url,
headers,
data,
responseType: "text",
};
if (files) {
const form = new FormData();
for (const [name, file] of Object.entries(files)) {
form.append(name, fs.createReadStream(file.path), {
filename: file.name,
contentType: "image/webp",
});
}
config.data = form;
config.headers = {
...headers,
...form.getHeaders(),
};
} else if (data) {
config.data = data;
}
try {
const response = await axios(config);
console.log(response.status);
console.log(response.data);
return response.data;
} catch (error) {
console.error("Error in processRequest:", error);
throw error;
}
}
async uploadImage(imagePath) {
const url =
"https://kwai-kolors-kolors.hf.space/upload?upload_id=uppaw4kwm5";
const headers = {
...this.commonHeaders,
"content-type": "multipart/form-data",
};
const responseText = await this.processRequest("post", url, headers, null, {
image: {
path: imagePath,
name: "image.webp",
},
});
const filePath = responseText.replace(/[\[\]"\\\n]/g, "");
return filePath;
}
async getJwtToken() {
const generateTimestamp = () =>
encodeURIComponent(new Date().toISOString());
const url = `https://huggingface.co/api/spaces/Kwai-Kolors/Kolors/jwt?expiration=${generateTimestamp()}`;
const responseText = await this.processRequest(
"get",
url,
this.commonHeaders
);
const responseJson = JSON.parse(responseText);
return responseJson.token;
}
async getQueueData(sessionHash) {
const url = `https://kwai-kolors-kolors.hf.space/queue/data?session_hash=${sessionHash}`;
const headers = {
...this.commonHeaders,
accept: "text/event-stream",
"content-type": "application/json",
};
try {
const response = await axios.get(url, {
headers,
responseType: "stream",
});
return new Promise((resolve, reject) => {
let timeoutId = setTimeout(() => {
response.data.destroy(); // Close the stream
resolve(sessionHash); // Return session_hash if timeout occurs
}, 3000); // 3 seconds timeout
response.data.on("data", (chunk) => {
const lines = chunk
.toString()
.split("\n")
.filter((line) => line.startsWith("data: "));
lines.forEach((line) => {
console.log(line);
const eventData = line.substring(6);
const eventJson = JSON.parse(eventData);
if (eventJson.msg === "process_completed") {
clearTimeout(timeoutId); // Clear the timeout
const outputData = eventJson.output?.data || [];
if (outputData.length > 0) {
const fileInfo = outputData[0];
resolve(fileInfo.url);
response.data.destroy(); // Close the stream
}
}
});
});
response.data.on("end", () => {
clearTimeout(timeoutId); // Clear the timeout
reject(new Error("Stream ended without completion"));
response.data.destroy(); // Close the stream
});
response.data.on("error", (err) => {
clearTimeout(timeoutId); // Clear the timeout
reject(err);
response.data.destroy(); // Close the stream
});
response.data.on("close", () => {
console.log("Stream closed");
});
});
} catch (error) {
console.error("Error in getQueueData:", error);
throw error;
}
}
async joinQueue(sessionHash, fileUrl, jwtToken, prompt) {
const url = "https://kwai-kolors-kolors.hf.space/queue/join?__theme=dark";
const headers = {
...this.commonHeaders,
"content-type": "application/json",
"x-zerogpu-token": jwtToken,
origin: "https://kwai-kolors-kolors.hf.space",
};
const data = {
data: [
prompt,
{
path: fileUrl,
url: `https://kwai-kolors-kolors.hf.space/file=${fileUrl}`,
orig_name: "image.webp",
size: 172602,
mime_type: "image/webp",
meta: {
_type: "gradio.FileData",
},
},
0.3,
"",
0,
true,
1024,
1536,
5,
25,
],
event_data: null,
fn_index: 2,
trigger_id: 26,
session_hash: sessionHash,
};
const responseText = await this.processRequest("post", url, headers, data);
return responseText;
}
generateSessionHash() {
const length = 11;
const charset =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let result = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length);
result += charset[randomIndex];
}
return result;
}
}
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Function to convert polygon data to SVG path
function polygonToSVGPath(polygon) {
if (!polygon || polygon.length === 0) {
return ""; // Return empty string if polygon is invalid
}
// Construct the SVG path string
let pathString = `M${polygon[0].x},${polygon[0].y}`; // Move to the first point
for (let i = 1; i < polygon.length; i++) {
pathString += ` L${polygon[i].x},${polygon[i].y}`; // Line to the next point
}
pathString += " Z"; // Close the path
return pathString;
}
function handleRequest(err, loadedFont, config) {
const [
text,
fontName,
size,
union,
filled,
kerning,
separate,
bezierAccuracy,
units,
fill,
stroke,
strokeWidth,
strokeNonScaling,
fillRule,
individualLetters,
res,
] = config;
if (err) {
console.error("Font could not be loaded:", err);
return res.status(500).json({ error: err.message });
}
if (individualLetters) {
// Generate individual SVGs for each letter and store them in a JSON object
const individualSVGs = {};
for (let i = 0; i < text.length; i++) {
const letter = text[i];
const result = callMakerjs(
loadedFont,
letter,
size,
union,
filled,
kerning,
separate,
bezierAccuracy,
units,
fill,
stroke,
strokeWidth,
strokeNonScaling,
fillRule
);
let temp;
xml2js.parseString(
result.svg,
{
explicitArray: false,
},
function (err, result) {
if (!err) {
// Now 'result' contains the JSON representation of the SVG
temp = result;
} else {
console.error(err);
}
}
);
individualSVGs[letter] = temp;
}
res.status(200).json(individualSVGs); // Return JSON object
} else {
// Generate a single SVG for the entire text
const result = callMakerjs(
loadedFont,
text,
size,
union,
filled,
kerning,
separate,
bezierAccuracy,
units,
fill,
stroke,
strokeWidth,
strokeNonScaling,
fillRule
);
res.status(200).send(result.svg);
}
}
async function downloadAndSaveFont(fontUrl) {
const tempDir = path.join(os.tmpdir(), "downloaded-or-converted-fonts");
try {
await fs.access(tempDir);
} catch (error) {
await fs.mkdir(tempDir, { recursive: true });
}
try {
const response = await axios.get(fontUrl, {
responseType: "arraybuffer",
headers: {
"Accept-Encoding": "identity",
},
});
let filename =
response.headers["content-disposition"]?.match(
/filename=['"]?(.+)['"]?/
)?.[1] || new URL(fontUrl).pathname.split("/").pop();
const fontPath = path.join(tempDir, filename);
await fs.writeFile(fontPath, response.data);
// Check if the file needs to be decompressed from WOFF2 to TTF
if (filename.endsWith(".woff2")) {
try {
const fontBuffer = await fs.readFile(fontPath);
const decompressedBuffer = await wawoff2.decompress(fontBuffer);
const ttfFilename = filename.replace(".woff2", ".ttf");
const ttfFontPath = path.join(tempDir, ttfFilename);
await fs.writeFile(ttfFontPath, decompressedBuffer);
console.log("Font decompressed and saved successfully.");
return ttfFontPath;
} catch (decompressionError) {
console.error("Error decompressing the font:", decompressionError);
throw decompressionError;
}
}
return fontPath;
} catch (error) {
console.error("Error processing the font:", error);
throw error;
}
}
function callMakerjs(
font,
text,
size,
union,
filled,
kerning,
separate,
bezierAccuracy,
units,
fill,
stroke,
strokeWidth,
strokeNonScaling,
fillRule
) {
// Generate the text using a font
var textModel = new makerjs.models.Text(
font,
text,
size,
union,
false,
bezierAccuracy,
{ kerning }
);
if (separate) {
for (var i in textModel.models) {
textModel.models[i].layer = i;
}
}
var svg = makerjs.exporter.toSVG(textModel, {
fill: filled ? fill : undefined,
stroke: stroke ? stroke : undefined,
strokeWidth: strokeWidth ? strokeWidth : undefined,
fillRule: fillRule ? fillRule : undefined,
scalingStroke: !strokeNonScaling,
});
var dxf = makerjs.exporter.toDXF(textModel, {
units: units,
usePOLYLINE: true,
});
return { svg, dxf };
}
// Helper function to download an image from a URL
async function downloadImage(url, dest) {
const writer = await fs.open(dest, "w");
const response = await axios.get(url, { responseType: "stream" });
await new Promise((resolve, reject) => {
response.data.pipe(writer.createWriteStream());
response.data.on("end", resolve);
response.data.on("error", reject);
});
await writer.close();
}
// API Endpoint
app.post("/highlight", express.json(), async (req, res) => {
const { imageUrl, searchTerms } = req.body;
if (!imageUrl || !Array.isArray(searchTerms) || searchTerms.length === 0) {
return res
.status(400)
.json({ error: "imageUrl and searchTerms are required" });
}
try {
// Download the image
const tempImagePath = path.join(os.tmpdir(), "temp_image.jpg");
await downloadImage(imageUrl, tempImagePath);
// Run OCR
const {
data: { text, words },
} = await Tesseract.recognize(tempImagePath, "eng", {
logger: (info) => console.log(info), // Optional: log OCR progress
});
const highlights = [];
// Search for each term
searchTerms.forEach((term) => {
const termWords = term.toLowerCase().split(" ");
const termLen = termWords.length;
let wordIndex = 0;
words.forEach((wordObj, i) => {
const word = wordObj.text?.toLowerCase();
if (!word) return;
if (word === termWords[wordIndex]) {
wordIndex++;
// If all words match
if (wordIndex === termLen) {
wordIndex = 0;
// Get bounding box
const xStart = words[i - termLen + 1].bbox.x0;
const yStart = words[i - termLen + 1].bbox.y0;
const xEnd = words[i].bbox.x1;
const yEnd = words[i].bbox.y1;
highlights.push({
text: term,
bbox: { x0: xStart, y0: yStart, x1: xEnd, y1: yEnd },
});
}
} else {
wordIndex = 0; // Reset if match breaks
}
});
});
// Clean up the temporary image
await fs.unlink(tempImagePath);
// Respond with highlights
return res.json({ searchTerms, highlights });
} catch (error) {
console.error(error);
return res
.status(500)
.json({ error: "An error occurred while processing the image." });
}
});
app.get("/process", async (req, res) => {
const kolors = new Kolors();
try {
const fileUrl =
"/tmp/gradio/a8afacda04e05001682bb475f128b24002ace7b7/e41b87fb-4cc3-43cd-a6e6-f3dbb08c2399.webp";
const prompt =
req.query.prompt ||
"Anna, 破旧衣服, 大声喊叫的愤怒脸, 嘴巴张大, 全身, 贫民窟背景, 仙女教母, 时尚TikTok风格衣服, 出现在闪亮和点赞的云中, 脸上带着炫酷的表情, 魔法棒, 变成华丽舞会礼服, 鱼网袜和蕾丝项圈, 震惊表情, 这幅艺术作品致敬了传奇的弗兰克·弗拉泽塔,展示了Loish van Baarle的独特风格和Boris Vallejo的动态笔触。这幅杰作向著名艺术家Ross Tran、Greg Tocchini、Tom Bagshaw和Steve Henderson的才华致敬,创造了一个引人入胜且迷人的场景。";
const sessionHash = req.query.session_hash || kolors.generateSessionHash();
// Step 1: Get JWT token
const jwtToken = await kolors.getJwtToken();
console.log("JWT Token:", jwtToken);
// Step 2: Join the queue
const queueResponse = await kolors.joinQueue(
sessionHash,
fileUrl,
jwtToken,
prompt
);
console.log("Queue Response:", queueResponse);
res.send({ sessionHash });
} catch (error) {
console.error("Error:", error);
res.status(500).send("An error occurred");
}
});
app.get("/poll", async (req, res) => {
const kolors = new Kolors();
try {
const sessionHash = req.query.session_hash;
if (!sessionHash) {
return res.status(400).send("session_hash is required");
}
const finalUrl = await kolors.getQueueData(sessionHash);
console.log("Final URL:", finalUrl);
res.send({ finalUrl });
} catch (error) {
console.error("Error:", error);
res.status(500).send("An error occurred");
}
});
app.post("/generateSVGPath", async (req, res) => {
// Set default values
const {
text,
size = 72, // Default font size
union = false,
filled = true,
kerning = true,
separate = false,
bezierAccuracy = 2,
units = "mm", // Default units
fill = "black", // Default fill color
stroke = "none", // Default stroke color
strokeWidth = "1", // Default stroke width
strokeNonScaling = false,
fillRule = "nonzero", // Default fill rule
fontUrl,
individualLetters = false,
font = "Roobert-Regular.ttf", // Default local font
} = req.body;
const config = [
text,
font,
size,
union,
filled,
kerning,
separate,
bezierAccuracy,
units,
fill,
stroke,
strokeWidth,
strokeNonScaling,
fillRule,
individualLetters,
res,
];
const fontPath = fontUrl
? await downloadAndSaveFont(fontUrl)
: path.join(__dirname, "public", "fonts", font);
console.log(fontPath);
try {
await fs.access(fontPath);
console.log("File exists:", fontPath);
} catch (error) {
console.log(fontPath);
res.status(500).json({ error: error.message });
// Handle the error or throw it
}
opentype.load(fontPath, (err, loadedFont) => {
handleRequest(err, loadedFont, config);
});
});
app.post("/generateSVGPathWithGoogleFont", async (req, res) => {
// Default values for parameters
const {
text,
fontName = "Open Sans", // Default Google Font
size = 72, // Default font size
union = false, // Default union
filled = true, // Default filled
kerning = true, // Default kerning
separate = false, // Default separate
bezierAccuracy = 2, // Default bezierAccuracy
units = "mm", // Default units
fill = "black", // Default fill color
stroke = "none", // Default stroke
strokeWidth = "1", // Default stroke width
strokeNonScaling = false, // Default strokeNonScaling
fillRule = "nonzero", // Default fillRule
individualLetters = false,
} = req.body;
const config = [
text,
fontName,
size,
union,
filled,
kerning,
separate,
bezierAccuracy,
units,
fill,
stroke,
strokeWidth,
strokeNonScaling,
fillRule,
individualLetters,
res,
];
const apiKey = "AIzaSyAOES8EmKhuJEnsn9kS1XKBpxxp-TgN8Jc"; // Use environment variable for API key
try {
// Fetch the list of fonts from Google Fonts API
const response = await axios.get(
`https://www.googleapis.com/webfonts/v1/webfonts?key=${apiKey}`
);
const fonts = response.data.items;
// Find the font with the given name
const fontDetails = fonts.find((f) => f.family === fontName);
if (!fontDetails) {
return res.status(404).send("Font not found");
}
// Load the font using opentype.js
let fontUrl = fontDetails.files.regular; // Adjust based on font variants if needed
fontUrl = fontUrl.replace("http", "https");
const fontPath = await downloadAndSaveFont(fontUrl);
opentype.load(fontPath, (err, loadedFont) => {
handleRequest(err, loadedFont, config);
});
} catch (error) {
console.error("Error:", error);
res.status(500).json({ error: error.message });
}
});
app.post("/predictions", async (req, res) => {
const { input, path } = req.body;
const headers = {
"Content-Type": "application/json",
// Add any other headers here
};
const data = {
input: input,
is_training: false,
create_model: "0",
stream: false,
};
try {
const response = await axios.post(
`https://replicate.com/api/${path}/predictions`,
data,
{ headers }
);
return res.json(response.data);
} catch (error) {
console.error("Error:", error);
return res.status(500).json({ error: error.message });
}
});
// Route handler for /vectorize
app.post("/vectorize", (req, res) => {
const { imageUrl } = req.body;
// Extract imageUrl from request body
// Call getImageOutline to get the polygon data
getImageOutline(imageUrl, function (err, polygon) {
if (err) {
console.error("Error:", err);
return res.status(500).json({ error: "Error vectorizing image" });
}
// Convert polygon data to SVG path
const svgPath = polygonToSVGPath(polygon);
// Send the SVG path back to the client as JSON
res.json({ svgPath });
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});