Skip to content

Commit d2ebd7d

Browse files
committed
feat(documentConverter): 为PDF转换添加水印功能支持
添加PDF水印配置选项,支持文字和图片水印 为PDF格式默认启用水印并设置默认配置 实现水印添加逻辑,包括文字水印和图片水印处理
1 parent e002f06 commit d2ebd7d

File tree

1 file changed

+156
-2
lines changed

1 file changed

+156
-2
lines changed

src/tools/documentConverter.ts

Lines changed: 156 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ interface ConversionOptions {
3232
text?: string;
3333
};
3434
};
35+
// PDF水印配置
36+
watermark?: {
37+
enabled?: boolean; // 是否启用水印,PDF格式默认为true
38+
text?: string; // 水印文字,默认为'doc-ops-mcp'
39+
imagePath?: string; // 水印图片路径,如果提供则优先使用图片
40+
opacity?: number; // 透明度,0-1之间,默认0.1
41+
fontSize?: number; // 文字水印字体大小,默认48
42+
rotation?: number; // 旋转角度,默认-45度
43+
spacing?: {
44+
x?: number; // 水印间距X,默认200
45+
y?: number; // 水印间距Y,默认150
46+
};
47+
};
3548
}
3649

3750
interface DocumentConversionResult {
@@ -72,13 +85,28 @@ export class DocumentConverter {
7285
const outputPath =
7386
options.outputPath ?? this.generateOutputPath(content.title ?? 'document', options.format);
7487

88+
// 为PDF格式设置默认水印配置
89+
if (options.format === 'pdf' && !options.watermark) {
90+
options.watermark = {
91+
enabled: true,
92+
text: 'doc-ops-mcp',
93+
opacity: 0.1,
94+
fontSize: 48,
95+
rotation: -45,
96+
spacing: {
97+
x: 200,
98+
y: 150
99+
}
100+
};
101+
}
102+
75103
switch (options.format) {
76104
case 'md':
77105
return await this.convertToMarkdown(content, outputPath);
78106
case 'html':
79107
return await this.convertToHTML(content, outputPath, styling);
80108
case 'pdf':
81-
return await this.convertToPDF(content, outputPath, styling);
109+
return await this.convertToPDF(content, outputPath, styling, options.watermark);
82110
case 'docx':
83111
return await this.convertToDocx(content, outputPath, styling);
84112
default:
@@ -301,7 +329,8 @@ export class DocumentConverter {
301329
private async convertToPDF(
302330
content: DocumentContent,
303331
outputPath: string,
304-
styling: any
332+
styling: any,
333+
watermarkConfig?: any
305334
): Promise<DocumentConversionResult> {
306335
try {
307336
// 使用pdf-lib直接生成PDF,类似Word转PDF的方式
@@ -364,6 +393,11 @@ export class DocumentConverter {
364393

365394
for (const line of lines) {
366395
if (yPosition < styling.margins.bottom + lineHeight) {
396+
// 为当前页面添加水印
397+
if (watermarkConfig?.enabled !== false) {
398+
await this.addWatermarkToPage(currentPage, watermarkConfig, pdfDoc);
399+
}
400+
367401
// 添加新页面
368402
currentPage = pdfDoc.addPage();
369403
yPosition = currentPage.getSize().height - styling.margins.top;
@@ -383,6 +417,11 @@ export class DocumentConverter {
383417
yPosition -= lineHeight * (line.isHeading ? 1.5 : 1);
384418
}
385419

420+
// 为最后一页添加水印
421+
if (watermarkConfig?.enabled !== false) {
422+
await this.addWatermarkToPage(currentPage, watermarkConfig, pdfDoc);
423+
}
424+
386425
const pdfBytes = await pdfDoc.save();
387426

388427
const writeFile = promisify(fs.writeFile);
@@ -1090,6 +1129,121 @@ export class DocumentConverter {
10901129
return rgb(0, 0, 0);
10911130
}
10921131

1132+
/**
1133+
* 为PDF页面添加水印
1134+
*/
1135+
private async addWatermarkToPage(page: any, watermarkConfig: any, pdfDoc: any): Promise<void> {
1136+
if (!watermarkConfig || watermarkConfig.enabled === false) {
1137+
return;
1138+
}
1139+
1140+
const { width, height } = page.getSize();
1141+
const { StandardFonts, rgb } = await import('pdf-lib');
1142+
1143+
// 水印配置默认值
1144+
const config = {
1145+
text: watermarkConfig.text || 'doc-ops-mcp',
1146+
opacity: watermarkConfig.opacity || 0.1,
1147+
fontSize: watermarkConfig.fontSize || 48,
1148+
rotation: watermarkConfig.rotation || -45,
1149+
spacing: {
1150+
x: watermarkConfig.spacing?.x || 200,
1151+
y: watermarkConfig.spacing?.y || 150
1152+
}
1153+
};
1154+
1155+
// 如果提供了图片路径,优先使用图片水印
1156+
if (watermarkConfig.imagePath && fs.existsSync(watermarkConfig.imagePath)) {
1157+
try {
1158+
const readFile = promisify(fs.readFile);
1159+
const imageBytes = await readFile(watermarkConfig.imagePath);
1160+
let image;
1161+
1162+
// 根据文件扩展名判断图片类型
1163+
const ext = watermarkConfig.imagePath.toLowerCase().split('.').pop();
1164+
if (ext === 'png') {
1165+
image = await pdfDoc.embedPng(imageBytes);
1166+
} else if (ext === 'jpg' || ext === 'jpeg') {
1167+
image = await pdfDoc.embedJpg(imageBytes);
1168+
} else {
1169+
console.warn('不支持的图片格式,使用文字水印');
1170+
await this.addTextWatermark(page, config, width, height, pdfDoc);
1171+
return;
1172+
}
1173+
1174+
// 绘制图片水印
1175+
const imageSize = Math.min(width, height) * 0.3; // 图片大小为页面最小边的30%
1176+
1177+
// 计算水印位置,规则铺满页面
1178+
for (let x = -imageSize; x < width + imageSize; x += config.spacing.x) {
1179+
for (let y = -imageSize; y < height + imageSize; y += config.spacing.y) {
1180+
page.drawImage(image, {
1181+
x: x,
1182+
y: y,
1183+
width: imageSize,
1184+
height: imageSize,
1185+
opacity: config.opacity,
1186+
rotate: {
1187+
type: 'degrees',
1188+
angle: config.rotation,
1189+
},
1190+
});
1191+
}
1192+
}
1193+
} catch (error) {
1194+
console.warn('图片水印添加失败,使用文字水印:', error);
1195+
await this.addTextWatermark(page, config, width, height, pdfDoc);
1196+
}
1197+
} else {
1198+
// 使用文字水印
1199+
await this.addTextWatermark(page, config, width, height, pdfDoc);
1200+
}
1201+
}
1202+
1203+
/**
1204+
* 添加文字水印
1205+
*/
1206+
private async addTextWatermark(page: any, config: any, width: number, height: number, pdfDoc?: any): Promise<void> {
1207+
const { StandardFonts, rgb } = await import('pdf-lib');
1208+
// 如果没有传入pdfDoc,尝试从page获取,否则创建新的字体
1209+
let font;
1210+
try {
1211+
font = await (pdfDoc || page.doc).embedFont(StandardFonts.Helvetica);
1212+
} catch {
1213+
// 如果无法获取字体,使用默认字体
1214+
font = StandardFonts.Helvetica;
1215+
}
1216+
1217+
// 计算文字水印的位置,斜着规则铺满页面
1218+
const textWidth = config.text.length * config.fontSize * 0.6; // 估算文字宽度
1219+
const textHeight = config.fontSize;
1220+
1221+
// 计算旋转后的实际占用空间
1222+
const radians = (config.rotation * Math.PI) / 180;
1223+
const cos = Math.abs(Math.cos(radians));
1224+
const sin = Math.abs(Math.sin(radians));
1225+
const rotatedWidth = textWidth * cos + textHeight * sin;
1226+
const rotatedHeight = textWidth * sin + textHeight * cos;
1227+
1228+
// 规则铺满页面
1229+
for (let x = -rotatedWidth; x < width + rotatedWidth; x += config.spacing.x) {
1230+
for (let y = -rotatedHeight; y < height + rotatedHeight; y += config.spacing.y) {
1231+
page.drawText(config.text, {
1232+
x: x,
1233+
y: y,
1234+
size: config.fontSize,
1235+
font: font,
1236+
color: rgb(0.5, 0.5, 0.5), // 灰色
1237+
opacity: config.opacity,
1238+
rotate: {
1239+
type: 'degrees',
1240+
angle: config.rotation,
1241+
},
1242+
});
1243+
}
1244+
}
1245+
}
1246+
10931247
/**
10941248
* 生成输出路径
10951249
*/

0 commit comments

Comments
 (0)