@@ -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
3750interface 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