( ′∀`)σ≡σ☆))Д′)レ(゚∀゚;)ヘ=З=З=Зε≡(ノ´_ゝ`)ノ
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const { Document, Packer, Paragraph, TextRun, ImageRun, FrameAnchorType, HorizontalPositionAlign, VerticalPositionAlign, AlignmentType } = require('docx');
const app = express();
const PORT = 3000;
// Increase limit for base64 images
app.use(bodyParser.json({ limit: '50mb' }));
app.use(cors());
/**
* Convert pixels to Twips (1/1440 inch)
* 1 px = 1/96 inch (approx)
* 1 inch = 1440 twips
* So 1 px = 15 twips
*/
const pxToTwips = (px) => Math.round(px * 15);
/**
* Convert pixels to Half-points (1/2 point)
* 1 px = 0.75 pt
* 1 pt = 2 half-points
* So 1 px = 1.5 half-points
*/
const pxToHalfPoints = (px) => Math.round(px * 1.5);
/**
* Clean hex color (remove #)
*/
const cleanColor = (color) => {
if (!color) return "000000";
if (color.startsWith('#')) return color.substring(1);
// Handle rgba or named colors if necessary (simplification: default to black if complex)
if (color.startsWith('rgb')) return "000000";
return color;
};
app.post('/export', async (req, res) => {
try {
const { width, height, backgroundDataUrl, objects } = req.body;
if (!backgroundDataUrl) {
return res.status(400).send("Missing backgroundDataUrl");
}
// Decode background image
// Data URL format: "data:image/png;base64,iVBOR..."
const base64Data = backgroundDataUrl.replace(/^data:image\/\w+;base64,/, "");
const imageBuffer = Buffer.from(base64Data, 'base64');
// Create paragraphs for text objects
const children = [];
// 1. Add Background Image (Floating, Behind Text)
// Note: In Word, "Behind Text" is achieved by floating the image with low z-index
// However, docx library handles floating images.
// We set the margins of the page to 0 and add the image first.
const bgImageRun = new ImageRun({
data: imageBuffer,
transformation: {
width: width, // pixels
height: height, // pixels
},
floating: {
horizontalPosition: {
offset: 0,
},
verticalPosition: {
offset: 0,
},
behindDocument: true, // This puts it behind text
},
});
children.push(new Paragraph({
children: [bgImageRun],
}));
// 2. Process Text Objects
if (objects && Array.isArray(objects)) {
objects.forEach(obj => {
// Filter for simple text: type 'text' or 'textbox'
// Skip if rotated (as per requirements)
if ((obj.type === 'text' || obj.type === 'textbox') && (!obj.angle || obj.angle === 0)) {
const fontSize = pxToHalfPoints(obj.fontSize * (obj.scaleY || 1));
const color = cleanColor(obj.fill);
// Fabric coordinates are top-left
// Word frames are also absolute
const x = pxToTwips(obj.left);
const y = pxToTwips(obj.top);
const w = pxToTwips(obj.width * (obj.scaleX || 1));
const h = pxToTwips(obj.height * (obj.scaleY || 1));
// Text Alignment
let alignment = AlignmentType.LEFT;
if (obj.textAlign === 'center') alignment = AlignmentType.CENTER;
if (obj.textAlign === 'right') alignment = AlignmentType.RIGHT;
if (obj.textAlign === 'justify') alignment = AlignmentType.JUSTIFIED;
// Create Paragraph with Frame
const p = new Paragraph({
children: [
new TextRun({
text: obj.text,
font: obj.fontFamily || "Arial",
size: fontSize, // half-points
color: color,
})
],
frame: {
type: "absolute",
position: {
x: x,
y: y,
},
width: w,
height: h, // exact height might clip, 'auto' is safer but 'atLeast' is common
anchor: {
horizontal: FrameAnchorType.PAGE,
vertical: FrameAnchorType.PAGE,
},
},
alignment: alignment,
});
children.push(p);
}
});
}
// Create Document
const doc = new Document({
sections: [{
properties: {
page: {
margin: {
top: 0,
right: 0,
bottom: 0,
left: 0,
},
size: {
width: pxToTwips(width),
height: pxToTwips(height),
}
},
},
children: children,
}],
});
// Generate Buffer
const b64string = await Packer.toBase64String(doc);
const buffer = Buffer.from(b64string, 'base64');
res.setHeader('Content-Disposition', 'attachment; filename=export.docx');
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
res.send(buffer);
} catch (error) {
console.error(error);
res.status(500).send("Error generating document: " + error.message);
}
});
app.listen(PORT, () => {
console.log(`Export server running on http://localhost:${PORT}`);
});