mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-08-20 13:47:46 +02:00
Feature/v2/watermark (#4215)
Add watermark feature Auto scroll on review --------- Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
This commit is contained in:
parent
acbebd67a3
commit
c1b7911518
@ -44,6 +44,7 @@
|
|||||||
"editYourNewFiles": "Edit your new file(s)",
|
"editYourNewFiles": "Edit your new file(s)",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"fileSelected": "Selected: {{filename}}",
|
"fileSelected": "Selected: {{filename}}",
|
||||||
|
"chooseFile": "Choose File",
|
||||||
"filesSelected": "{{count}} files selected",
|
"filesSelected": "{{count}} files selected",
|
||||||
"files": {
|
"files": {
|
||||||
"title": "Files",
|
"title": "Files",
|
||||||
@ -777,26 +778,180 @@
|
|||||||
"submit": "Add image"
|
"submit": "Add image"
|
||||||
},
|
},
|
||||||
"watermark": {
|
"watermark": {
|
||||||
"tags": "Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo",
|
|
||||||
"title": "Add Watermark",
|
"title": "Add Watermark",
|
||||||
"header": "Add Watermark",
|
"desc": "Add text or image watermarks to PDF files",
|
||||||
"customColor": "Custom Text Colour",
|
"completed": "Watermark added",
|
||||||
"selectText": {
|
|
||||||
"1": "Select PDF to add watermark to:",
|
|
||||||
"2": "Watermark Text:",
|
|
||||||
"3": "Font Size:",
|
|
||||||
"4": "Rotation (0-360):",
|
|
||||||
"5": "Width Spacer (Space between each watermark horizontally):",
|
|
||||||
"6": "Height Spacer (Space between each watermark vertically):",
|
|
||||||
"7": "Opacity (0% - 100%):",
|
|
||||||
"8": "Watermark Type:",
|
|
||||||
"9": "Watermark Image:",
|
|
||||||
"10": "Convert PDF to PDF-Image"
|
|
||||||
},
|
|
||||||
"submit": "Add Watermark",
|
"submit": "Add Watermark",
|
||||||
"type": {
|
"filenamePrefix": "watermarked",
|
||||||
"1": "Text",
|
"error": {
|
||||||
"2": "Image"
|
"failed": "An error occurred while adding watermark to the PDF."
|
||||||
|
},
|
||||||
|
"watermarkType": {
|
||||||
|
"text": "Text",
|
||||||
|
"image": "Image"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"type": "Watermark Type",
|
||||||
|
"text": {
|
||||||
|
"label": "Watermark Text",
|
||||||
|
"placeholder": "Enter watermark text"
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"label": "Watermark Image",
|
||||||
|
"choose": "Choose Image",
|
||||||
|
"selected": "Selected: {{filename}}"
|
||||||
|
},
|
||||||
|
"fontSize": "Font Size",
|
||||||
|
"size": "Size",
|
||||||
|
"alphabet": "Font/Language",
|
||||||
|
"color": "Watermark Colour",
|
||||||
|
"rotation": "Rotation (degrees)",
|
||||||
|
"opacity": "Opacity (%)",
|
||||||
|
"spacing": {
|
||||||
|
"horizontal": "Horizontal Spacing",
|
||||||
|
"vertical": "Vertical Spacing"
|
||||||
|
},
|
||||||
|
"convertToImage": "Flatten PDF pages to images"
|
||||||
|
},
|
||||||
|
"alphabet": {
|
||||||
|
"roman": "Roman/Latin",
|
||||||
|
"arabic": "Arabic",
|
||||||
|
"japanese": "Japanese",
|
||||||
|
"korean": "Korean",
|
||||||
|
"chinese": "Chinese",
|
||||||
|
"thai": "Thai"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"type": "Watermark Type",
|
||||||
|
"wording": "Wording",
|
||||||
|
"textStyle": "Style",
|
||||||
|
"formatting": "Formatting",
|
||||||
|
"file": "Watermark File"
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"title": "Watermark Results"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"language": {
|
||||||
|
"title": "Language Support",
|
||||||
|
"text": "Choose the appropriate language setting to ensure proper font rendering for your text."
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"title": "Appearance Settings",
|
||||||
|
"text": "Control how your watermark looks and blends with the document.",
|
||||||
|
"bullet1": "Rotation: -360° to 360° for angled watermarks",
|
||||||
|
"bullet2": "Opacity: 0-100% for transparency control",
|
||||||
|
"bullet3": "Lower opacity creates subtle watermarks"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"title": "Spacing Control",
|
||||||
|
"text": "Adjust the spacing between repeated watermarks across the page.",
|
||||||
|
"bullet1": "Width spacing: Horizontal distance between watermarks",
|
||||||
|
"bullet2": "Height spacing: Vertical distance between watermarks",
|
||||||
|
"bullet3": "Higher values create more spread out patterns"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"header": {
|
||||||
|
"title": "Watermark Type Selection"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"title": "Choose Your Watermark",
|
||||||
|
"text": "Select between text or image watermarks based on your needs."
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"title": "Text Watermarks",
|
||||||
|
"text": "Perfect for adding copyright notices, company names, or confidentiality labels. Supports multiple languages and custom colours.",
|
||||||
|
"bullet1": "Customisable fonts and languages",
|
||||||
|
"bullet2": "Adjustable colours and transparency",
|
||||||
|
"bullet3": "Ideal for legal or branding text"
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"title": "Image Watermarks",
|
||||||
|
"text": "Use logos, stamps, or any image as a watermark. Great for branding and visual identification.",
|
||||||
|
"bullet1": "Upload any image format",
|
||||||
|
"bullet2": "Maintains image quality",
|
||||||
|
"bullet3": "Perfect for logos and stamps"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wording": {
|
||||||
|
"header": {
|
||||||
|
"title": "Text Content"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"title": "Watermark Text",
|
||||||
|
"text": "Enter the text that will appear as your watermark across the document.",
|
||||||
|
"bullet1": "Keep it concise for better readability",
|
||||||
|
"bullet2": "Common examples: 'CONFIDENTIAL', 'DRAFT', company name",
|
||||||
|
"bullet3": "Emoji characters are not supported and will be filtered out"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"textStyle": {
|
||||||
|
"header": {
|
||||||
|
"title": "Text Style"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"title": "Colour Selection",
|
||||||
|
"text": "Choose a colour that provides good contrast with your document content.",
|
||||||
|
"bullet1": "Light grey (#d3d3d3) for subtle watermarks",
|
||||||
|
"bullet2": "Black or dark colours for high contrast",
|
||||||
|
"bullet3": "Custom colours for branding purposes"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"title": "Language Support",
|
||||||
|
"text": "Choose the appropriate language setting to ensure proper font rendering."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file": {
|
||||||
|
"header": {
|
||||||
|
"title": "Image Upload"
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"title": "Image Selection",
|
||||||
|
"text": "Upload an image file to use as your watermark.",
|
||||||
|
"bullet1": "Supports common formats: PNG, JPG, GIF, BMP",
|
||||||
|
"bullet2": "PNG with transparency works best",
|
||||||
|
"bullet3": "Higher resolution images maintain quality better"
|
||||||
|
},
|
||||||
|
"recommendations": {
|
||||||
|
"title": "Best Practices",
|
||||||
|
"text": "Tips for optimal image watermark results.",
|
||||||
|
"bullet1": "Use logos or stamps with transparent backgrounds",
|
||||||
|
"bullet2": "Simple designs work better than complex images",
|
||||||
|
"bullet3": "Consider the final document size when choosing resolution"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatting": {
|
||||||
|
"header": {
|
||||||
|
"title": "Formatting & Layout"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"title": "Size Control",
|
||||||
|
"text": "Adjust the size of your watermark (text or image).",
|
||||||
|
"bullet1": "Larger sizes create more prominent watermarks"
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"title": "Appearance Settings",
|
||||||
|
"text": "Control how your watermark looks and blends with the document.",
|
||||||
|
"bullet1": "Rotation: -360° to 360° for angled watermarks",
|
||||||
|
"bullet2": "Opacity: 0-100% for transparency control",
|
||||||
|
"bullet3": "Lower opacity creates subtle watermarks"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"title": "Spacing Control",
|
||||||
|
"text": "Adjust the spacing between repeated watermarks across the page.",
|
||||||
|
"bullet1": "Horizontal spacing: Distance between watermarks left to right",
|
||||||
|
"bullet2": "Vertical spacing: Distance between watermarks top to bottom",
|
||||||
|
"bullet3": "Higher values create more spread out patterns"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security Option",
|
||||||
|
"text": "Convert the final PDF to an image-based format for enhanced security.",
|
||||||
|
"bullet1": "Prevents text selection and copying",
|
||||||
|
"bullet2": "Makes watermarks harder to remove",
|
||||||
|
"bullet3": "Results in larger file sizes",
|
||||||
|
"bullet4": "Best for sensitive or copyrighted content"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"permissions": {
|
"permissions": {
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
"download": "Download",
|
"download": "Download",
|
||||||
"editYourNewFiles": "Edit your new file(s)",
|
"editYourNewFiles": "Edit your new file(s)",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
|
"chooseFile": "Choose File",
|
||||||
"fileSelected": "Selected: {{filename}}",
|
"fileSelected": "Selected: {{filename}}",
|
||||||
"filesSelected": "{{count}} files selected",
|
"filesSelected": "{{count}} files selected",
|
||||||
"files": {
|
"files": {
|
||||||
@ -717,29 +718,6 @@
|
|||||||
"upload": "Add image",
|
"upload": "Add image",
|
||||||
"submit": "Add image"
|
"submit": "Add image"
|
||||||
},
|
},
|
||||||
"watermark": {
|
|
||||||
"tags": "Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo",
|
|
||||||
"title": "Add Watermark",
|
|
||||||
"header": "Add Watermark",
|
|
||||||
"customColor": "Custom Text Color",
|
|
||||||
"selectText": {
|
|
||||||
"1": "Select PDF to add watermark to:",
|
|
||||||
"2": "Watermark Text:",
|
|
||||||
"3": "Font Size:",
|
|
||||||
"4": "Rotation (0-360):",
|
|
||||||
"5": "Width Spacer (Space between each watermark horizontally):",
|
|
||||||
"6": "Height Spacer (Space between each watermark vertically):",
|
|
||||||
"7": "Opacity (0% - 100%):",
|
|
||||||
"8": "Watermark Type:",
|
|
||||||
"9": "Watermark Image:",
|
|
||||||
"10": "Convert PDF to PDF-Image"
|
|
||||||
},
|
|
||||||
"submit": "Add Watermark",
|
|
||||||
"type": {
|
|
||||||
"1": "Text",
|
|
||||||
"2": "Image"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"tags": "read,write,edit,print",
|
"tags": "read,write,edit,print",
|
||||||
"title": "Change Permissions",
|
"title": "Change Permissions",
|
||||||
@ -1742,6 +1720,245 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"watermark": {
|
||||||
|
"tags": "Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo",
|
||||||
|
"title": "Add Watermark",
|
||||||
|
"desc": "Add text or image watermarks to PDF files",
|
||||||
|
"header": "Add Watermark",
|
||||||
|
"completed": "Watermark added",
|
||||||
|
"submit": "Add Watermark",
|
||||||
|
"filenamePrefix": "watermarked",
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred while adding watermark to the PDF."
|
||||||
|
},
|
||||||
|
"watermarkType": {
|
||||||
|
"text": "Text",
|
||||||
|
"image": "Image"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"type": "Watermark Type",
|
||||||
|
"text": {
|
||||||
|
"label": "Watermark Text",
|
||||||
|
"placeholder": "Enter watermark text"
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"label": "Watermark Image",
|
||||||
|
"choose": "Choose Image",
|
||||||
|
"selected": "Selected: {{filename}}"
|
||||||
|
},
|
||||||
|
"fontSize": "Font Size",
|
||||||
|
"alphabet": "Font/Language",
|
||||||
|
"color": "Watermark Color",
|
||||||
|
"rotation": "Rotation (degrees)",
|
||||||
|
"opacity": "Opacity (%)",
|
||||||
|
"spacing": {
|
||||||
|
"horizontal": "Horizontal Spacing",
|
||||||
|
"vertical": "Vertical Spacing"
|
||||||
|
},
|
||||||
|
"convertToImage": "Flatten PDF pages to images"
|
||||||
|
},
|
||||||
|
"alphabet": {
|
||||||
|
"roman": "Roman/Latin",
|
||||||
|
"arabic": "Arabic",
|
||||||
|
"japanese": "Japanese",
|
||||||
|
"korean": "Korean",
|
||||||
|
"chinese": "Chinese",
|
||||||
|
"thai": "Thai"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"type": "Watermark Type",
|
||||||
|
"wording": "Wording",
|
||||||
|
"textStyle": "Style",
|
||||||
|
"file": "Watermark File",
|
||||||
|
"formatting": "Formatting"
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"title": "Watermark Results"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"language": {
|
||||||
|
"title": "Language Support",
|
||||||
|
"text": "Choose the appropriate language setting to ensure proper font rendering for your text."
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"title": "Appearance Settings",
|
||||||
|
"text": "Control how your watermark looks and blends with the document.",
|
||||||
|
"bullet1": "Rotation: -360° to 360° for angled watermarks",
|
||||||
|
"bullet2": "Opacity: 0-100% for transparency control",
|
||||||
|
"bullet3": "Lower opacity creates subtle watermarks"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"title": "Spacing Control",
|
||||||
|
"text": "Adjust the spacing between repeated watermarks across the page.",
|
||||||
|
"bullet1": "Width spacing: Horizontal distance between watermarks",
|
||||||
|
"bullet2": "Height spacing: Vertical distance between watermarks",
|
||||||
|
"bullet3": "Higher values create more spread out patterns"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"header": {
|
||||||
|
"title": "Watermark Type Selection"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"title": "Choose Your Watermark",
|
||||||
|
"text": "Select between text or image watermarks based on your needs."
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"title": "Text Watermarks",
|
||||||
|
"text": "Perfect for adding copyright notices, company names, or confidentiality labels. Supports multiple languages and custom colors.",
|
||||||
|
"bullet1": "Customizable fonts and languages",
|
||||||
|
"bullet2": "Adjustable colors and transparency",
|
||||||
|
"bullet3": "Ideal for legal or branding text"
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"title": "Image Watermarks",
|
||||||
|
"text": "Use logos, stamps, or any image as a watermark. Great for branding and visual identification.",
|
||||||
|
"bullet1": "Upload any image format",
|
||||||
|
"bullet2": "Maintains image quality",
|
||||||
|
"bullet3": "Perfect for logos and stamps"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"header": {
|
||||||
|
"title": "Content Configuration"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"title": "Text Settings",
|
||||||
|
"text": "Configure your text watermark appearance and language support.",
|
||||||
|
"bullet1": "Enter your watermark text",
|
||||||
|
"bullet2": "Adjust font size (8-72pt)",
|
||||||
|
"bullet3": "Select language/script support",
|
||||||
|
"bullet4": "Choose custom colors"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"title": "Language Support",
|
||||||
|
"text": "Choose the appropriate language setting to ensure proper font rendering for your text.",
|
||||||
|
"bullet1": "Roman/Latin for Western languages",
|
||||||
|
"bullet2": "Arabic for Arabic script",
|
||||||
|
"bullet3": "Japanese, Korean, Chinese for Asian languages",
|
||||||
|
"bullet4": "Thai for Thai script"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"header": {
|
||||||
|
"title": "Style & Positioning"
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"title": "Appearance Settings",
|
||||||
|
"text": "Control how your watermark looks and blends with the document.",
|
||||||
|
"bullet1": "Rotation: -360° to 360° for angled watermarks",
|
||||||
|
"bullet2": "Opacity: 0-100% for transparency control",
|
||||||
|
"bullet3": "Lower opacity creates subtle watermarks"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"title": "Spacing Control",
|
||||||
|
"text": "Adjust the spacing between repeated watermarks across the page.",
|
||||||
|
"bullet1": "Width spacing: Horizontal distance between watermarks",
|
||||||
|
"bullet2": "Height spacing: Vertical distance between watermarks",
|
||||||
|
"bullet3": "Higher values create more spread out patterns"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wording": {
|
||||||
|
"header": {
|
||||||
|
"title": "Text Content"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"title": "Watermark Text",
|
||||||
|
"text": "Enter the text that will appear as your watermark across the document.",
|
||||||
|
"bullet1": "Keep it concise for better readability",
|
||||||
|
"bullet2": "Common examples: 'CONFIDENTIAL', 'DRAFT', company name",
|
||||||
|
"bullet3": "Emoji characters are not supported and will be filtered out"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"textStyle": {
|
||||||
|
"header": {
|
||||||
|
"title": "Text Style"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"title": "Language Support",
|
||||||
|
"text": "Choose the appropriate language setting to ensure proper font rendering.",
|
||||||
|
"bullet1": "Roman/Latin for Western languages",
|
||||||
|
"bullet2": "Arabic for Arabic script",
|
||||||
|
"bullet3": "Japanese, Korean, Chinese for Asian languages",
|
||||||
|
"bullet4": "Thai for Thai script"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"title": "Color Selection",
|
||||||
|
"text": "Choose a color that provides good contrast with your document content.",
|
||||||
|
"bullet1": "Light gray (#d3d3d3) for subtle watermarks",
|
||||||
|
"bullet2": "Black or dark colors for high contrast",
|
||||||
|
"bullet3": "Custom colors for branding purposes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file": {
|
||||||
|
"header": {
|
||||||
|
"title": "Image Upload"
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"title": "Image Selection",
|
||||||
|
"text": "Upload an image file to use as your watermark.",
|
||||||
|
"bullet1": "Supports common formats: PNG, JPG, GIF, BMP",
|
||||||
|
"bullet2": "PNG with transparency works best",
|
||||||
|
"bullet3": "Higher resolution images maintain quality better"
|
||||||
|
},
|
||||||
|
"recommendations": {
|
||||||
|
"title": "Best Practices",
|
||||||
|
"text": "Tips for optimal image watermark results.",
|
||||||
|
"bullet1": "Use logos or stamps with transparent backgrounds",
|
||||||
|
"bullet2": "Simple designs work better than complex images",
|
||||||
|
"bullet3": "Consider the final document size when choosing resolution"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatting": {
|
||||||
|
"header": {
|
||||||
|
"title": "Formatting & Layout"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"title": "Size Control",
|
||||||
|
"text": "Adjust the size of your watermark (text or image).",
|
||||||
|
"bullet1": "Larger sizes create more prominent watermarks"
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"title": "Appearance Settings",
|
||||||
|
"text": "Control how your watermark looks and blends with the document.",
|
||||||
|
"bullet1": "Rotation: -360° to 360° for angled watermarks",
|
||||||
|
"bullet2": "Opacity: 0-100% for transparency control",
|
||||||
|
"bullet3": "Lower opacity creates subtle watermarks"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"title": "Spacing Control",
|
||||||
|
"text": "Adjust the spacing between repeated watermarks across the page.",
|
||||||
|
"bullet1": "Horizontal spacing: Distance between watermarks left to right",
|
||||||
|
"bullet2": "Vertical spacing: Distance between watermarks top to bottom",
|
||||||
|
"bullet3": "Higher values create more spread out patterns"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security Option",
|
||||||
|
"text": "Flatten PDF pages to images for enhanced security.",
|
||||||
|
"bullet1": "Prevents text selection and copying",
|
||||||
|
"bullet2": "Makes watermarks harder to remove",
|
||||||
|
"bullet3": "Results in larger file sizes",
|
||||||
|
"bullet4": "Best for sensitive or copyrighted content"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"header": {
|
||||||
|
"title": "Advanced Options"
|
||||||
|
},
|
||||||
|
"conversion": {
|
||||||
|
"title": "PDF to Image Conversion",
|
||||||
|
"text": "Convert the final PDF to an image-based format for enhanced security.",
|
||||||
|
"bullet1": "Prevents text selection and copying",
|
||||||
|
"bullet2": "Makes watermarks harder to remove",
|
||||||
|
"bullet3": "Results in larger file sizes",
|
||||||
|
"bullet4": "Best for sensitive or copyrighted content"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security Considerations",
|
||||||
|
"text": "Image-based PDFs provide additional protection against unauthorized editing and content extraction."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"removePassword": {
|
"removePassword": {
|
||||||
"title": "Remove Password",
|
"title": "Remove Password",
|
||||||
"desc": "Remove password protection from your PDF document.",
|
"desc": "Remove password protection from your PDF document.",
|
||||||
|
45
frontend/src/components/shared/FileUploadButton.tsx
Normal file
45
frontend/src/components/shared/FileUploadButton.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React, { useRef } from "react";
|
||||||
|
import { FileButton, Button } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
interface FileUploadButtonProps {
|
||||||
|
file?: File;
|
||||||
|
onChange: (file: File | null) => void;
|
||||||
|
accept?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
variant?: "outline" | "filled" | "light" | "default" | "subtle" | "gradient";
|
||||||
|
fullWidth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileUploadButton = ({
|
||||||
|
file,
|
||||||
|
onChange,
|
||||||
|
accept = "*/*",
|
||||||
|
disabled = false,
|
||||||
|
placeholder,
|
||||||
|
variant = "outline",
|
||||||
|
fullWidth = true
|
||||||
|
}: FileUploadButtonProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const resetRef = useRef<() => void>(null);
|
||||||
|
|
||||||
|
const defaultPlaceholder = t('chooseFile', 'Choose File');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileButton
|
||||||
|
resetRef={resetRef}
|
||||||
|
onChange={onChange}
|
||||||
|
accept={accept}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{(props) => (
|
||||||
|
<Button {...props} variant={variant} fullWidth={fullWidth}>
|
||||||
|
{file ? file.name : (placeholder || defaultPlaceholder)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</FileButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileUploadButton;
|
@ -2,7 +2,8 @@ import React, { useState, useRef, useEffect } from 'react';
|
|||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { isClickOutside, addEventListenerWithCleanup } from '../../utils/genericUtils';
|
import { isClickOutside, addEventListenerWithCleanup } from '../../utils/genericUtils';
|
||||||
import { useTooltipPosition } from '../../hooks/useTooltipPosition';
|
import { useTooltipPosition } from '../../hooks/useTooltipPosition';
|
||||||
import { TooltipContent, TooltipTip } from './tooltip/TooltipContent';
|
import { TooltipTip } from '../../types/tips';
|
||||||
|
import { TooltipContent } from './tooltip/TooltipContent';
|
||||||
import { useSidebarContext } from '../../contexts/SidebarContext';
|
import { useSidebarContext } from '../../contexts/SidebarContext';
|
||||||
import styles from './tooltip/Tooltip.module.css'
|
import styles from './tooltip/Tooltip.module.css'
|
||||||
|
|
||||||
|
@ -1,12 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styles from './Tooltip.module.css';
|
import styles from './Tooltip.module.css';
|
||||||
|
import { TooltipTip } from '../../../types/tips';
|
||||||
export interface TooltipTip {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
bullets?: string[];
|
|
||||||
body?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TooltipContentProps {
|
interface TooltipContentProps {
|
||||||
content?: React.ReactNode;
|
content?: React.ReactNode;
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Stack, Checkbox, Group } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||||
|
import NumberInputWithUnit from "../shared/NumberInputWithUnit";
|
||||||
|
|
||||||
|
interface WatermarkFormattingProps {
|
||||||
|
parameters: AddWatermarkParameters;
|
||||||
|
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WatermarkFormatting = ({ parameters, onParameterChange, disabled = false }: WatermarkFormattingProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Size - single row */}
|
||||||
|
<NumberInputWithUnit
|
||||||
|
label={t('watermark.settings.size', 'Size')}
|
||||||
|
value={parameters.fontSize}
|
||||||
|
onChange={(value) => onParameterChange('fontSize', typeof value === 'number' ? value : 12)}
|
||||||
|
unit={parameters.watermarkType === 'text' ? 'pt' : 'px'}
|
||||||
|
min={1}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Position & Appearance - 2 per row */}
|
||||||
|
<Group grow align="flex-start">
|
||||||
|
<NumberInputWithUnit
|
||||||
|
label={t('watermark.settings.rotation', 'Rotation')}
|
||||||
|
value={parameters.rotation}
|
||||||
|
onChange={(value) => onParameterChange('rotation', typeof value === 'number' ? value : 0)}
|
||||||
|
unit="°"
|
||||||
|
min={-360}
|
||||||
|
max={360}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<NumberInputWithUnit
|
||||||
|
label={t('watermark.settings.opacity', 'Opacity')}
|
||||||
|
value={parameters.opacity}
|
||||||
|
onChange={(value) => onParameterChange('opacity', typeof value === 'number' ? value : 50)}
|
||||||
|
unit="%"
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Spacing - 2 per row */}
|
||||||
|
<Group grow align="flex-start">
|
||||||
|
<NumberInputWithUnit
|
||||||
|
label={t('watermark.settings.spacing.horizontal', 'Horizontal Spacing')}
|
||||||
|
value={parameters.widthSpacer}
|
||||||
|
onChange={(value) => onParameterChange('widthSpacer', typeof value === 'number' ? value : 50)}
|
||||||
|
unit="px"
|
||||||
|
min={0}
|
||||||
|
max={200}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<NumberInputWithUnit
|
||||||
|
label={t('watermark.settings.spacing.vertical', 'Vertical Spacing')}
|
||||||
|
value={parameters.heightSpacer}
|
||||||
|
onChange={(value) => onParameterChange('heightSpacer', typeof value === 'number' ? value : 50)}
|
||||||
|
unit="px"
|
||||||
|
min={0}
|
||||||
|
max={200}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Advanced Options */}
|
||||||
|
<Checkbox
|
||||||
|
label={t('watermark.settings.convertToImage', 'Flatten PDF pages to images')}
|
||||||
|
checked={parameters.convertPDFToImage}
|
||||||
|
onChange={(event) => onParameterChange('convertPDFToImage', event.currentTarget.checked)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WatermarkFormatting;
|
@ -0,0 +1,29 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Stack } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||||
|
import FileUploadButton from "../../shared/FileUploadButton";
|
||||||
|
|
||||||
|
interface WatermarkImageFileProps {
|
||||||
|
parameters: AddWatermarkParameters;
|
||||||
|
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WatermarkImageFile = ({ parameters, onParameterChange, disabled = false }: WatermarkImageFileProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<FileUploadButton
|
||||||
|
file={parameters.watermarkImage}
|
||||||
|
onChange={(file) => onParameterChange('watermarkImage', file)}
|
||||||
|
accept="image/*"
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={t('watermark.settings.image.choose', 'Choose Image')}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WatermarkImageFile;
|
@ -0,0 +1,63 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Stack, Text, NumberInput } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||||
|
|
||||||
|
interface WatermarkStyleSettingsProps {
|
||||||
|
parameters: AddWatermarkParameters;
|
||||||
|
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = false }: WatermarkStyleSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Appearance Settings */}
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Text size="sm" fw={500}>{t('watermark.settings.rotation', 'Rotation (degrees)')}</Text>
|
||||||
|
<NumberInput
|
||||||
|
value={parameters.rotation}
|
||||||
|
onChange={(value) => onParameterChange('rotation', value || 0)}
|
||||||
|
min={-360}
|
||||||
|
max={360}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text size="sm" fw={500}>{t('watermark.settings.opacity', 'Opacity (%)')}</Text>
|
||||||
|
<NumberInput
|
||||||
|
value={parameters.opacity}
|
||||||
|
onChange={(value) => onParameterChange('opacity', value || 50)}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Spacing Settings */}
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Text size="sm" fw={500}>{t('watermark.settings.spacing.width', 'Width Spacing')}</Text>
|
||||||
|
<NumberInput
|
||||||
|
value={parameters.widthSpacer}
|
||||||
|
onChange={(value) => onParameterChange('widthSpacer', value || 50)}
|
||||||
|
min={0}
|
||||||
|
max={200}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text size="sm" fw={500}>{t('watermark.settings.spacing.height', 'Height Spacing')}</Text>
|
||||||
|
<NumberInput
|
||||||
|
value={parameters.heightSpacer}
|
||||||
|
onChange={(value) => onParameterChange('heightSpacer', value || 50)}
|
||||||
|
min={0}
|
||||||
|
max={200}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WatermarkStyleSettings;
|
@ -0,0 +1,46 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Stack, Text, Select, ColorInput } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||||
|
import { alphabetOptions } from "../../../constants/addWatermarkConstants";
|
||||||
|
|
||||||
|
interface WatermarkTextStyleProps {
|
||||||
|
parameters: AddWatermarkParameters;
|
||||||
|
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WatermarkTextStyle = ({ parameters, onParameterChange, disabled = false }: WatermarkTextStyleProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text size="xs" fw={500}>
|
||||||
|
{t("watermark.settings.color", "Colour")}
|
||||||
|
</Text>
|
||||||
|
<ColorInput
|
||||||
|
value={parameters.customColor}
|
||||||
|
onChange={(value) => onParameterChange("customColor", value)}
|
||||||
|
disabled={disabled}
|
||||||
|
format="hex"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text size="xs" fw={500}>
|
||||||
|
{t("watermark.settings.alphabet", "Alphabet")}
|
||||||
|
</Text>
|
||||||
|
<Select
|
||||||
|
value={parameters.alphabet}
|
||||||
|
onChange={(value) => value && onParameterChange("alphabet", value)}
|
||||||
|
data={alphabetOptions}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WatermarkTextStyle;
|
@ -0,0 +1,44 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button, Stack, Text } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
interface WatermarkTypeSettingsProps {
|
||||||
|
watermarkType?: 'text' | 'image';
|
||||||
|
onWatermarkTypeChange: (type: 'text' | 'image') => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WatermarkTypeSettings = ({ watermarkType, onWatermarkTypeChange, disabled = false }: WatermarkTypeSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
|
<Button
|
||||||
|
variant={watermarkType === 'text' ? 'filled' : 'outline'}
|
||||||
|
color={watermarkType === 'text' ? 'blue' : 'gray'}
|
||||||
|
onClick={() => onWatermarkTypeChange('text')}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||||
|
{t('watermark.watermarkType.text', 'Text')}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={watermarkType === 'image' ? 'filled' : 'outline'}
|
||||||
|
color={watermarkType === 'image' ? 'blue' : 'gray'}
|
||||||
|
onClick={() => onWatermarkTypeChange('image')}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||||
|
{t('watermark.watermarkType.image', 'Image')}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WatermarkTypeSettings;
|
@ -0,0 +1,34 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Stack, Text, TextInput } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||||
|
import { removeEmojis } from "../../../utils/textUtils";
|
||||||
|
|
||||||
|
interface WatermarkWordingProps {
|
||||||
|
parameters: AddWatermarkParameters;
|
||||||
|
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WatermarkWording = ({ parameters, onParameterChange, disabled = false }: WatermarkWordingProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
const filteredValue = removeEmojis(value);
|
||||||
|
onParameterChange('watermarkText', filteredValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<TextInput
|
||||||
|
placeholder={t('watermark.settings.text.placeholder', 'Enter watermark text')}
|
||||||
|
value={parameters.watermarkText}
|
||||||
|
onChange={handleTextChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WatermarkWording;
|
57
frontend/src/components/tools/shared/NumberInputWithUnit.tsx
Normal file
57
frontend/src/components/tools/shared/NumberInputWithUnit.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Stack, Text, NumberInput } from "@mantine/core";
|
||||||
|
|
||||||
|
interface NumberInputWithUnitProps {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
onChange: (value: number | string) => void;
|
||||||
|
unit: string;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NumberInputWithUnit = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
unit,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
disabled = false
|
||||||
|
}: NumberInputWithUnitProps) => {
|
||||||
|
const [localValue, setLocalValue] = useState<number | string>(value);
|
||||||
|
|
||||||
|
// Sync local value when external value changes
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
onChange(localValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="xs" style={{ flex: 1 }}>
|
||||||
|
<Text size="xs" fw={500} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<NumberInput
|
||||||
|
value={localValue}
|
||||||
|
onChange={setLocalValue}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
disabled={disabled}
|
||||||
|
rightSection={
|
||||||
|
<Text size="sm" c="dimmed" pr="sm">
|
||||||
|
{unit}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
rightSectionWidth={unit.length * 8 + 20} // Dynamic width based on unit length
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NumberInputWithUnit;
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { Button, Stack, Text } from '@mantine/core';
|
import { Button, Stack, Text } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import DownloadIcon from '@mui/icons-material/Download';
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
@ -14,24 +14,32 @@ export interface ReviewToolStepProps<TParams = unknown> {
|
|||||||
onFileClick?: (file: File) => void;
|
onFileClick?: (file: File) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createReviewToolStep<TParams = unknown>(
|
function ReviewStepContent<TParams = unknown>({ operation, onFileClick }: { operation: ToolOperationHook<TParams>; onFileClick?: (file: File) => void }) {
|
||||||
createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement,
|
|
||||||
props: ReviewToolStepProps<TParams>
|
|
||||||
): React.ReactElement {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { operation } = props;
|
const stepRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const previewFiles = operation.files?.map((file, index) => ({
|
const previewFiles = operation.files?.map((file, index) => ({
|
||||||
file,
|
file,
|
||||||
thumbnail: operation.thumbnails[index]
|
thumbnail: operation.thumbnails[index]
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
return createStep(t("review", "Review"), {
|
// Auto-scroll to bottom when content appears
|
||||||
isVisible: props.isVisible,
|
useEffect(() => {
|
||||||
_excludeFromCount: true,
|
if (stepRef.current && (previewFiles.length > 0 || operation.downloadUrl || operation.errorMessage)) {
|
||||||
_noPadding: true
|
const scrollableContainer = stepRef.current.closest('[style*="overflow: auto"]') as HTMLElement;
|
||||||
}, (
|
if (scrollableContainer) {
|
||||||
<Stack gap="sm" >
|
setTimeout(() => {
|
||||||
|
scrollableContainer.scrollTo({
|
||||||
|
top: scrollableContainer.scrollHeight,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}, 100); // Small delay to ensure content is rendered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [previewFiles.length, operation.downloadUrl, operation.errorMessage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm" ref={stepRef}>
|
||||||
<ErrorNotification
|
<ErrorNotification
|
||||||
error={operation.errorMessage}
|
error={operation.errorMessage}
|
||||||
onClose={operation.clearError}
|
onClose={operation.clearError}
|
||||||
@ -40,7 +48,7 @@ export function createReviewToolStep<TParams = unknown>(
|
|||||||
{previewFiles.length > 0 && (
|
{previewFiles.length > 0 && (
|
||||||
<ResultsPreview
|
<ResultsPreview
|
||||||
files={previewFiles}
|
files={previewFiles}
|
||||||
onFileClick={props.onFileClick}
|
onFileClick={onFileClick}
|
||||||
isGeneratingThumbnails={operation.isGeneratingThumbnails}
|
isGeneratingThumbnails={operation.isGeneratingThumbnails}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -61,5 +69,23 @@ export function createReviewToolStep<TParams = unknown>(
|
|||||||
|
|
||||||
<SuggestedToolsSection />
|
<SuggestedToolsSection />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createReviewToolStep<TParams = unknown>(
|
||||||
|
createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement,
|
||||||
|
props: ReviewToolStepProps<TParams>
|
||||||
|
): React.ReactElement {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return createStep(t("review", "Review"), {
|
||||||
|
isVisible: props.isVisible,
|
||||||
|
_excludeFromCount: true,
|
||||||
|
_noPadding: true
|
||||||
|
}, (
|
||||||
|
<ReviewStepContent
|
||||||
|
operation={props.operation}
|
||||||
|
onFileClick={props.onFileClick}
|
||||||
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,13 @@ import { Text, Stack, Box, Flex, Divider } from '@mantine/core';
|
|||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||||
import { Tooltip } from '../../shared/Tooltip';
|
import { Tooltip } from '../../shared/Tooltip';
|
||||||
import { TooltipTip } from '../../shared/tooltip/TooltipContent';
|
import { TooltipTip } from '../../../types/tips';
|
||||||
import { createFilesToolStep, FilesToolStepProps } from './FilesToolStep';
|
import { createFilesToolStep, FilesToolStepProps } from './FilesToolStep';
|
||||||
import { createReviewToolStep, ReviewToolStepProps } from './ReviewToolStep';
|
import { createReviewToolStep, ReviewToolStepProps } from './ReviewToolStep';
|
||||||
|
|
||||||
interface ToolStepContextType {
|
interface ToolStepContextType {
|
||||||
visibleStepCount: number;
|
visibleStepCount: number;
|
||||||
|
forceStepNumbers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolStepContext = createContext<ToolStepContextType | null>(null);
|
const ToolStepContext = createContext<ToolStepContextType | null>(null);
|
||||||
@ -82,10 +83,11 @@ const ToolStep = ({
|
|||||||
|
|
||||||
const parent = useContext(ToolStepContext);
|
const parent = useContext(ToolStepContext);
|
||||||
|
|
||||||
// Auto-detect if we should show numbers based on sibling count
|
// Auto-detect if we should show numbers based on sibling count or force option
|
||||||
const shouldShowNumber = useMemo(() => {
|
const shouldShowNumber = useMemo(() => {
|
||||||
if (showNumber !== undefined) return showNumber;
|
if (showNumber !== undefined) return showNumber; // Individual step override
|
||||||
return parent ? parent.visibleStepCount >= 3 : false;
|
if (parent?.forceStepNumbers) return true; // Flow-level force
|
||||||
|
return parent ? parent.visibleStepCount >= 3 : false; // Auto-detect
|
||||||
}, [showNumber, parent]);
|
}, [showNumber, parent]);
|
||||||
|
|
||||||
const stepNumber = _stepNumber;
|
const stepNumber = _stepNumber;
|
||||||
@ -196,7 +198,7 @@ export function createToolSteps() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Context provider wrapper for tools using the factory
|
// Context provider wrapper for tools using the factory
|
||||||
export function ToolStepProvider({ children }: { children: React.ReactNode }) {
|
export function ToolStepProvider({ children, forceStepNumbers }: { children: React.ReactNode; forceStepNumbers?: boolean }) {
|
||||||
// Count visible steps from children that are ToolStep elements
|
// Count visible steps from children that are ToolStep elements
|
||||||
const visibleStepCount = useMemo(() => {
|
const visibleStepCount = useMemo(() => {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@ -212,8 +214,9 @@ export function ToolStepProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}, [children]);
|
}, [children]);
|
||||||
|
|
||||||
const contextValue = useMemo(() => ({
|
const contextValue = useMemo(() => ({
|
||||||
visibleStepCount
|
visibleStepCount,
|
||||||
}), [visibleStepCount]);
|
forceStepNumbers
|
||||||
|
}), [visibleStepCount, forceStepNumbers]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolStepContext.Provider value={contextValue}>
|
<ToolStepContext.Provider value={contextValue}>
|
||||||
|
@ -49,6 +49,7 @@ export interface ToolFlowConfig {
|
|||||||
steps: MiddleStepConfig[];
|
steps: MiddleStepConfig[];
|
||||||
executeButton?: ExecuteButtonConfig;
|
executeButton?: ExecuteButtonConfig;
|
||||||
review: ReviewStepConfig;
|
review: ReviewStepConfig;
|
||||||
|
forceStepNumbers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,7 +61,7 @@ export function createToolFlow(config: ToolFlowConfig) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="sm" p="sm" h="95vh" w="100%" style={{ overflow: 'auto' }}>
|
<Stack gap="sm" p="sm" h="95vh" w="100%" style={{ overflow: 'auto' }}>
|
||||||
<ToolStepProvider>
|
<ToolStepProvider forceStepNumbers={config.forceStepNumbers}>
|
||||||
{/* Files Step */}
|
{/* Files Step */}
|
||||||
{steps.createFilesStep({
|
{steps.createFilesStep({
|
||||||
selectedFiles: config.files.selectedFiles,
|
selectedFiles: config.files.selectedFiles,
|
||||||
|
176
frontend/src/components/tooltips/useWatermarkTips.ts
Normal file
176
frontend/src/components/tooltips/useWatermarkTips.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { TooltipContent, TooltipTip } from '../../types/tips';
|
||||||
|
|
||||||
|
// Shared tooltip content to reduce duplication
|
||||||
|
const useSharedWatermarkContent = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const languageSupportTip: TooltipTip = {
|
||||||
|
title: t("watermark.tooltip.language.title", "Language Support"),
|
||||||
|
description: t("watermark.tooltip.language.text", "Choose the appropriate language setting to ensure proper font rendering for your text.")
|
||||||
|
};
|
||||||
|
|
||||||
|
const appearanceTip: TooltipTip = {
|
||||||
|
title: t("watermark.tooltip.appearance.title", "Appearance Settings"),
|
||||||
|
description: t("watermark.tooltip.appearance.text", "Control how your watermark looks and blends with the document."),
|
||||||
|
bullets: [
|
||||||
|
t("watermark.tooltip.appearance.bullet1", "Rotation: -360° to 360° for angled watermarks"),
|
||||||
|
t("watermark.tooltip.appearance.bullet2", "Opacity: 0-100% for transparency control"),
|
||||||
|
t("watermark.tooltip.appearance.bullet3", "Lower opacity creates subtle watermarks")
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const spacingTip: TooltipTip = {
|
||||||
|
title: t("watermark.tooltip.spacing.title", "Spacing Control"),
|
||||||
|
description: t("watermark.tooltip.spacing.text", "Adjust the spacing between repeated watermarks across the page."),
|
||||||
|
bullets: [
|
||||||
|
t("watermark.tooltip.spacing.bullet1", "Width spacing: Horizontal distance between watermarks"),
|
||||||
|
t("watermark.tooltip.spacing.bullet2", "Height spacing: Vertical distance between watermarks"),
|
||||||
|
t("watermark.tooltip.spacing.bullet3", "Higher values create more spread out patterns")
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
return { languageSupportTip, appearanceTip, spacingTip };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useWatermarkTypeTips = (): TooltipContent => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: {
|
||||||
|
title: t("watermark.tooltip.type.header.title", "Watermark Type Selection")
|
||||||
|
},
|
||||||
|
tips: [
|
||||||
|
{
|
||||||
|
title: t("watermark.tooltip.type.description.title", "Choose Your Watermark"),
|
||||||
|
description: t("watermark.tooltip.type.description.text", "Select between text or image watermarks based on your needs.")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("watermark.tooltip.type.text.title", "Text Watermarks"),
|
||||||
|
description: t("watermark.tooltip.type.text.text", "Perfect for adding copyright notices, company names, or confidentiality labels. Supports multiple languages and custom colors."),
|
||||||
|
bullets: [
|
||||||
|
t("watermark.tooltip.type.text.bullet1", "Customizable fonts and languages"),
|
||||||
|
t("watermark.tooltip.type.text.bullet2", "Adjustable colors and transparency"),
|
||||||
|
t("watermark.tooltip.type.text.bullet3", "Ideal for legal or branding text")
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("watermark.tooltip.type.image.title", "Image Watermarks"),
|
||||||
|
description: t("watermark.tooltip.type.image.text", "Use logos, stamps, or any image as a watermark. Great for branding and visual identification."),
|
||||||
|
bullets: [
|
||||||
|
t("watermark.tooltip.type.image.bullet1", "Upload any image format"),
|
||||||
|
t("watermark.tooltip.type.image.bullet2", "Maintains image quality"),
|
||||||
|
t("watermark.tooltip.type.image.bullet3", "Perfect for logos and stamps")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const useWatermarkWordingTips = (): TooltipContent => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: {
|
||||||
|
title: t("watermark.tooltip.wording.header.title", "Text Content")
|
||||||
|
},
|
||||||
|
tips: [
|
||||||
|
{
|
||||||
|
title: t("watermark.tooltip.wording.text.title", "Watermark Text"),
|
||||||
|
description: t("watermark.tooltip.wording.text.text", "Enter the text that will appear as your watermark across the document."),
|
||||||
|
bullets: [
|
||||||
|
t("watermark.tooltip.wording.text.bullet1", "Keep it concise for better readability"),
|
||||||
|
t("watermark.tooltip.wording.text.bullet2", "Common examples: 'CONFIDENTIAL', 'DRAFT', company name"),
|
||||||
|
t("watermark.tooltip.wording.text.bullet3", "Emoji characters are not supported and will be filtered out")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useWatermarkTextStyleTips = (): TooltipContent => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { languageSupportTip } = useSharedWatermarkContent();
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: {
|
||||||
|
title: t("watermark.tooltip.textStyle.header.title", "Text Style")
|
||||||
|
},
|
||||||
|
tips: [
|
||||||
|
{
|
||||||
|
title: t("watermark.tooltip.textStyle.color.title", "Color Selection"),
|
||||||
|
description: t("watermark.tooltip.textStyle.color.text", "Choose a color that provides good contrast with your document content."),
|
||||||
|
bullets: [
|
||||||
|
t("watermark.tooltip.textStyle.color.bullet1", "Light gray (#d3d3d3) for subtle watermarks"),
|
||||||
|
t("watermark.tooltip.textStyle.color.bullet2", "Black or dark colors for high contrast"),
|
||||||
|
t("watermark.tooltip.textStyle.color.bullet3", "Custom colors for branding purposes")
|
||||||
|
]
|
||||||
|
},
|
||||||
|
languageSupportTip
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useWatermarkFileTips = (): TooltipContent => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: {
|
||||||
|
title: t("watermark.tooltip.file.header.title", "Image Upload")
|
||||||
|
},
|
||||||
|
tips: [
|
||||||
|
{
|
||||||
|
title: t("watermark.tooltip.file.upload.title", "Image Selection"),
|
||||||
|
description: t("watermark.tooltip.file.upload.text", "Upload an image file to use as your watermark."),
|
||||||
|
bullets: [
|
||||||
|
t("watermark.tooltip.file.upload.bullet1", "Supports common formats: PNG, JPG, GIF, BMP"),
|
||||||
|
t("watermark.tooltip.file.upload.bullet2", "PNG with transparency works best"),
|
||||||
|
t("watermark.tooltip.file.upload.bullet3", "Higher resolution images maintain quality better")
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("watermark.tooltip.file.recommendations.title", "Best Practices"),
|
||||||
|
description: t("watermark.tooltip.file.recommendations.text", "Tips for optimal image watermark results."),
|
||||||
|
bullets: [
|
||||||
|
t("watermark.tooltip.file.recommendations.bullet1", "Use logos or stamps with transparent backgrounds"),
|
||||||
|
t("watermark.tooltip.file.recommendations.bullet2", "Simple designs work better than complex images"),
|
||||||
|
t("watermark.tooltip.file.recommendations.bullet3", "Consider the final document size when choosing resolution")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useWatermarkFormattingTips = (): TooltipContent => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { appearanceTip, spacingTip } = useSharedWatermarkContent();
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: {
|
||||||
|
title: t("watermark.tooltip.formatting.header.title", "Formatting & Layout")
|
||||||
|
},
|
||||||
|
tips: [
|
||||||
|
{
|
||||||
|
title: t("watermark.tooltip.formatting.size.title", "Size Control"),
|
||||||
|
description: t("watermark.tooltip.formatting.size.text", "Adjust the size of your watermark (text or image)."),
|
||||||
|
bullets: [
|
||||||
|
t("watermark.tooltip.formatting.size.bullet1", "Larger sizes create more prominent watermarks")
|
||||||
|
]
|
||||||
|
},
|
||||||
|
appearanceTip,
|
||||||
|
spacingTip,
|
||||||
|
{
|
||||||
|
title: t("watermark.tooltip.formatting.security.title", "Security Option"),
|
||||||
|
description: t("watermark.tooltip.formatting.security.text", "Convert the final PDF to an image-based format for enhanced security."),
|
||||||
|
bullets: [
|
||||||
|
t("watermark.tooltip.formatting.security.bullet1", "Prevents text selection and copying"),
|
||||||
|
t("watermark.tooltip.formatting.security.bullet2", "Makes watermarks harder to remove"),
|
||||||
|
t("watermark.tooltip.formatting.security.bullet3", "Results in larger file sizes"),
|
||||||
|
t("watermark.tooltip.formatting.security.bullet4", "Best for sensitive or copyrighted content")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
28
frontend/src/constants/addWatermarkConstants.ts
Normal file
28
frontend/src/constants/addWatermarkConstants.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { AddWatermarkParameters } from "../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||||
|
|
||||||
|
export interface AlphabetOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const alphabetOptions: AlphabetOption[] = [
|
||||||
|
{ value: "roman", label: "Roman" },
|
||||||
|
{ value: "arabic", label: "العربية" },
|
||||||
|
{ value: "japanese", label: "日本語" },
|
||||||
|
{ value: "korean", label: "한국어" },
|
||||||
|
{ value: "chinese", label: "简体中文" },
|
||||||
|
{ value: "thai", label: "ไทย" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const defaultWatermarkParameters: AddWatermarkParameters = {
|
||||||
|
watermarkType: undefined,
|
||||||
|
watermarkText: '',
|
||||||
|
fontSize: 12,
|
||||||
|
rotation: 0,
|
||||||
|
opacity: 50,
|
||||||
|
widthSpacer: 50,
|
||||||
|
heightSpacer: 50,
|
||||||
|
alphabet: 'roman',
|
||||||
|
customColor: '#d3d3d3',
|
||||||
|
convertPDFToImage: false
|
||||||
|
};
|
@ -0,0 +1,46 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
|
import { AddWatermarkParameters } from './useAddWatermarkParameters';
|
||||||
|
|
||||||
|
const buildFormData = (parameters: AddWatermarkParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
|
||||||
|
// Required: watermarkType as string
|
||||||
|
formData.append("watermarkType", parameters.watermarkType || "text");
|
||||||
|
|
||||||
|
// Add watermark content based on type
|
||||||
|
if (parameters.watermarkType === 'text') {
|
||||||
|
formData.append("watermarkText", parameters.watermarkText);
|
||||||
|
} else if (parameters.watermarkType === 'image' && parameters.watermarkImage) {
|
||||||
|
formData.append("watermarkImage", parameters.watermarkImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required parameters with correct formatting
|
||||||
|
formData.append("fontSize", parameters.fontSize.toString());
|
||||||
|
formData.append("rotation", parameters.rotation.toString());
|
||||||
|
formData.append("opacity", (parameters.opacity / 100).toString()); // Convert percentage to decimal
|
||||||
|
formData.append("widthSpacer", parameters.widthSpacer.toString());
|
||||||
|
formData.append("heightSpacer", parameters.heightSpacer.toString());
|
||||||
|
|
||||||
|
// Backend-expected parameters from user input
|
||||||
|
formData.append("alphabet", parameters.alphabet);
|
||||||
|
formData.append("customColor", parameters.customColor);
|
||||||
|
formData.append("convertPDFToImage", parameters.convertPDFToImage.toString());
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAddWatermarkOperation = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return useToolOperation<AddWatermarkParameters>({
|
||||||
|
operationType: 'watermark',
|
||||||
|
endpoint: '/api/v1/security/add-watermark',
|
||||||
|
buildFormData,
|
||||||
|
filePrefix: t('watermark.filenamePrefix', 'watermarked') + '_',
|
||||||
|
multiFileEndpoint: false, // Individual API calls per file
|
||||||
|
getErrorMessage: createStandardErrorHandler(t('watermark.error.failed', 'An error occurred while adding watermark to the PDF.'))
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,50 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { defaultWatermarkParameters } from '../../../constants/addWatermarkConstants';
|
||||||
|
|
||||||
|
export interface AddWatermarkParameters {
|
||||||
|
watermarkType?: 'text' | 'image';
|
||||||
|
watermarkText: string;
|
||||||
|
watermarkImage?: File;
|
||||||
|
fontSize: number; // Used for both text size and image size
|
||||||
|
rotation: number;
|
||||||
|
opacity: number;
|
||||||
|
widthSpacer: number;
|
||||||
|
heightSpacer: number;
|
||||||
|
alphabet: string;
|
||||||
|
customColor: string;
|
||||||
|
convertPDFToImage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const useAddWatermarkParameters = () => {
|
||||||
|
const [parameters, setParameters] = useState<AddWatermarkParameters>(defaultWatermarkParameters);
|
||||||
|
|
||||||
|
const updateParameter = useCallback(<K extends keyof AddWatermarkParameters>(
|
||||||
|
key: K,
|
||||||
|
value: AddWatermarkParameters[K]
|
||||||
|
) => {
|
||||||
|
setParameters(prev => ({ ...prev, [key]: value }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetParameters = useCallback(() => {
|
||||||
|
setParameters(defaultWatermarkParameters);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const validateParameters = useCallback((): boolean => {
|
||||||
|
if (!parameters.watermarkType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (parameters.watermarkType === 'text') {
|
||||||
|
return parameters.watermarkText.trim().length > 0;
|
||||||
|
} else {
|
||||||
|
return parameters.watermarkImage !== undefined;
|
||||||
|
}
|
||||||
|
}, [parameters]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
parameters,
|
||||||
|
updateParameter,
|
||||||
|
resetParameters,
|
||||||
|
validateParameters
|
||||||
|
};
|
||||||
|
};
|
@ -6,6 +6,7 @@ import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
|
|||||||
import ApiIcon from "@mui/icons-material/Api";
|
import ApiIcon from "@mui/icons-material/Api";
|
||||||
import CleaningServicesIcon from "@mui/icons-material/CleaningServices";
|
import CleaningServicesIcon from "@mui/icons-material/CleaningServices";
|
||||||
import LockIcon from "@mui/icons-material/Lock";
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
|
import BrandingWatermarkIcon from "@mui/icons-material/BrandingWatermark";
|
||||||
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
||||||
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
||||||
import { Tool, ToolDefinition, ToolRegistry } from "../types/tool";
|
import { Tool, ToolDefinition, ToolRegistry } from "../types/tool";
|
||||||
@ -105,6 +106,15 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|||||||
description: "Change document restrictions and permissions",
|
description: "Change document restrictions and permissions",
|
||||||
endpoints: ["add-password"]
|
endpoints: ["add-password"]
|
||||||
},
|
},
|
||||||
|
watermark: {
|
||||||
|
id: "watermark",
|
||||||
|
icon: <BrandingWatermarkIcon />,
|
||||||
|
component: React.lazy(() => import("../tools/AddWatermark")),
|
||||||
|
maxFiles: -1,
|
||||||
|
category: "security",
|
||||||
|
description: "Add text or image watermarks to PDF files",
|
||||||
|
endpoints: ["add-watermark"]
|
||||||
|
},
|
||||||
removePassword: {
|
removePassword: {
|
||||||
id: "removePassword",
|
id: "removePassword",
|
||||||
icon: <LockOpenIcon />,
|
icon: <LockOpenIcon />,
|
||||||
|
212
frontend/src/tools/AddWatermark.tsx
Normal file
212
frontend/src/tools/AddWatermark.tsx
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
|
import { useFileContext } from "../contexts/FileContext";
|
||||||
|
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||||
|
|
||||||
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
|
|
||||||
|
import WatermarkTypeSettings from "../components/tools/addWatermark/WatermarkTypeSettings";
|
||||||
|
import WatermarkWording from "../components/tools/addWatermark/WatermarkWording";
|
||||||
|
import WatermarkTextStyle from "../components/tools/addWatermark/WatermarkTextStyle";
|
||||||
|
import WatermarkImageFile from "../components/tools/addWatermark/WatermarkImageFile";
|
||||||
|
import WatermarkFormatting from "../components/tools/addWatermark/WatermarkFormatting";
|
||||||
|
|
||||||
|
import { useAddWatermarkParameters } from "../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||||
|
import { useAddWatermarkOperation } from "../hooks/tools/addWatermark/useAddWatermarkOperation";
|
||||||
|
import {
|
||||||
|
useWatermarkTypeTips,
|
||||||
|
useWatermarkWordingTips,
|
||||||
|
useWatermarkTextStyleTips,
|
||||||
|
useWatermarkFileTips,
|
||||||
|
useWatermarkFormattingTips,
|
||||||
|
} from "../components/tooltips/useWatermarkTips";
|
||||||
|
import { BaseToolProps } from "../types/tool";
|
||||||
|
|
||||||
|
const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setCurrentMode } = useFileContext();
|
||||||
|
const { selectedFiles } = useToolFileSelection();
|
||||||
|
|
||||||
|
const [collapsedType, setCollapsedType] = useState(false);
|
||||||
|
const [collapsedStyle, setCollapsedStyle] = useState(true);
|
||||||
|
const [collapsedFormatting, setCollapsedFormatting] = useState(true);
|
||||||
|
|
||||||
|
const watermarkParams = useAddWatermarkParameters();
|
||||||
|
const watermarkOperation = useAddWatermarkOperation();
|
||||||
|
const watermarkTypeTips = useWatermarkTypeTips();
|
||||||
|
const watermarkWordingTips = useWatermarkWordingTips();
|
||||||
|
const watermarkTextStyleTips = useWatermarkTextStyleTips();
|
||||||
|
const watermarkFileTips = useWatermarkFileTips();
|
||||||
|
const watermarkFormattingTips = useWatermarkFormattingTips();
|
||||||
|
|
||||||
|
// Endpoint validation
|
||||||
|
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("add-watermark");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
watermarkOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
}, [watermarkParams.parameters]);
|
||||||
|
|
||||||
|
// Auto-collapse type step after selection
|
||||||
|
useEffect(() => {
|
||||||
|
if (watermarkParams.parameters.watermarkType && !collapsedType) {
|
||||||
|
setCollapsedType(true);
|
||||||
|
}
|
||||||
|
}, [watermarkParams.parameters.watermarkType]);
|
||||||
|
|
||||||
|
const handleAddWatermark = async () => {
|
||||||
|
try {
|
||||||
|
await watermarkOperation.executeOperation(watermarkParams.parameters, selectedFiles);
|
||||||
|
if (watermarkOperation.files && onComplete) {
|
||||||
|
onComplete(watermarkOperation.files);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (onError) {
|
||||||
|
onError(error instanceof Error ? error.message : t("watermark.error.failed", "Add watermark operation failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThumbnailClick = (file: File) => {
|
||||||
|
onPreviewFile?.(file);
|
||||||
|
sessionStorage.setItem("previousMode", "watermark");
|
||||||
|
setCurrentMode("viewer");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSettingsReset = () => {
|
||||||
|
watermarkOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
setCurrentMode("watermark");
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasFiles = selectedFiles.length > 0;
|
||||||
|
const hasResults = watermarkOperation.files.length > 0 || watermarkOperation.downloadUrl !== null;
|
||||||
|
|
||||||
|
// Dynamic step structure based on watermark type
|
||||||
|
const getSteps = () => {
|
||||||
|
const steps = [];
|
||||||
|
|
||||||
|
steps.push({
|
||||||
|
title: t("watermark.steps.type", "Watermark Type"),
|
||||||
|
isCollapsed: hasResults ? true : collapsedType,
|
||||||
|
isVisible: hasFiles || hasResults,
|
||||||
|
onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedType(!collapsedType),
|
||||||
|
tooltip: watermarkTypeTips,
|
||||||
|
content: (
|
||||||
|
<WatermarkTypeSettings
|
||||||
|
watermarkType={watermarkParams.parameters.watermarkType}
|
||||||
|
onWatermarkTypeChange={(type) => watermarkParams.updateParameter("watermarkType", type)}
|
||||||
|
disabled={endpointLoading}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasFiles || hasResults) {
|
||||||
|
// Text watermark path
|
||||||
|
if (watermarkParams.parameters.watermarkType === "text") {
|
||||||
|
// Step 2: Wording
|
||||||
|
steps.push({
|
||||||
|
title: t("watermark.steps.wording", "Wording"),
|
||||||
|
isCollapsed: hasResults,
|
||||||
|
tooltip: watermarkWordingTips,
|
||||||
|
content: (
|
||||||
|
<WatermarkWording
|
||||||
|
parameters={watermarkParams.parameters}
|
||||||
|
onParameterChange={watermarkParams.updateParameter}
|
||||||
|
disabled={endpointLoading}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 3: Style
|
||||||
|
steps.push({
|
||||||
|
title: t("watermark.steps.textStyle", "Style"),
|
||||||
|
isCollapsed: hasResults ? true : collapsedStyle,
|
||||||
|
onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedStyle(!collapsedStyle),
|
||||||
|
tooltip: watermarkTextStyleTips,
|
||||||
|
content: (
|
||||||
|
<WatermarkTextStyle
|
||||||
|
parameters={watermarkParams.parameters}
|
||||||
|
onParameterChange={watermarkParams.updateParameter}
|
||||||
|
disabled={endpointLoading}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 4: Formatting
|
||||||
|
steps.push({
|
||||||
|
title: t("watermark.steps.formatting", "Formatting"),
|
||||||
|
isCollapsed: hasResults ? true : collapsedFormatting,
|
||||||
|
onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedFormatting(!collapsedFormatting),
|
||||||
|
tooltip: watermarkFormattingTips,
|
||||||
|
content: (
|
||||||
|
<WatermarkFormatting
|
||||||
|
parameters={watermarkParams.parameters}
|
||||||
|
onParameterChange={watermarkParams.updateParameter}
|
||||||
|
disabled={endpointLoading}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image watermark path
|
||||||
|
if (watermarkParams.parameters.watermarkType === "image") {
|
||||||
|
// Step 2: Watermark File
|
||||||
|
steps.push({
|
||||||
|
title: t("watermark.steps.file", "Watermark File"),
|
||||||
|
isCollapsed: hasResults,
|
||||||
|
tooltip: watermarkFileTips,
|
||||||
|
content: (
|
||||||
|
<WatermarkImageFile
|
||||||
|
parameters={watermarkParams.parameters}
|
||||||
|
onParameterChange={watermarkParams.updateParameter}
|
||||||
|
disabled={endpointLoading}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 3: Formatting
|
||||||
|
steps.push({
|
||||||
|
title: t("watermark.steps.formatting", "Formatting"),
|
||||||
|
isCollapsed: hasResults ? true : collapsedFormatting,
|
||||||
|
onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedFormatting(!collapsedFormatting),
|
||||||
|
tooltip: watermarkFormattingTips,
|
||||||
|
content: (
|
||||||
|
<WatermarkFormatting
|
||||||
|
parameters={watermarkParams.parameters}
|
||||||
|
onParameterChange={watermarkParams.updateParameter}
|
||||||
|
disabled={endpointLoading}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps;
|
||||||
|
};
|
||||||
|
|
||||||
|
return createToolFlow({
|
||||||
|
files: {
|
||||||
|
selectedFiles,
|
||||||
|
isCollapsed: hasFiles || hasResults,
|
||||||
|
},
|
||||||
|
steps: getSteps(),
|
||||||
|
executeButton: {
|
||||||
|
text: t("watermark.submit", "Add Watermark"),
|
||||||
|
isVisible: !hasResults,
|
||||||
|
loadingText: t("loading"),
|
||||||
|
onClick: handleAddWatermark,
|
||||||
|
disabled: !watermarkParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||||
|
},
|
||||||
|
review: {
|
||||||
|
isVisible: hasResults,
|
||||||
|
operation: watermarkOperation,
|
||||||
|
title: t("watermark.results.title", "Watermark Results"),
|
||||||
|
onFileClick: handleThumbnailClick,
|
||||||
|
},
|
||||||
|
forceStepNumbers: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddWatermark;
|
@ -17,6 +17,7 @@ export type ModeType =
|
|||||||
| 'sanitize'
|
| 'sanitize'
|
||||||
| 'addPassword'
|
| 'addPassword'
|
||||||
| 'changePermissions'
|
| 'changePermissions'
|
||||||
|
| 'watermark'
|
||||||
| 'removePassword';
|
| 'removePassword';
|
||||||
|
|
||||||
export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor';
|
export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor';
|
||||||
@ -108,12 +109,12 @@ export interface FileContextActions {
|
|||||||
removeFiles: (fileIds: string[], deleteFromStorage?: boolean) => void;
|
removeFiles: (fileIds: string[], deleteFromStorage?: boolean) => void;
|
||||||
replaceFile: (oldFileId: string, newFile: File) => Promise<void>;
|
replaceFile: (oldFileId: string, newFile: File) => Promise<void>;
|
||||||
clearAllFiles: () => void;
|
clearAllFiles: () => void;
|
||||||
|
|
||||||
// File pinning
|
// File pinning
|
||||||
pinFile: (file: File) => void;
|
pinFile: (file: File) => void;
|
||||||
unpinFile: (file: File) => void;
|
unpinFile: (file: File) => void;
|
||||||
isFilePinned: (file: File) => boolean;
|
isFilePinned: (file: File) => boolean;
|
||||||
|
|
||||||
// File consumption (replace unpinned files with outputs)
|
// File consumption (replace unpinned files with outputs)
|
||||||
consumeFiles: (inputFiles: File[], outputFiles: File[]) => Promise<void>;
|
consumeFiles: (inputFiles: File[], outputFiles: File[]) => Promise<void>;
|
||||||
|
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
|
export interface TooltipTip {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
bullets?: string[];
|
||||||
|
body?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TooltipContent {
|
export interface TooltipContent {
|
||||||
header?: {
|
header?: {
|
||||||
title: string;
|
title: string;
|
||||||
logo?: string | React.ReactNode;
|
logo?: string | React.ReactNode;
|
||||||
};
|
};
|
||||||
tips?: Array<{
|
tips?: TooltipTip[];
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
bullets?: string[];
|
|
||||||
body?: React.ReactNode;
|
|
||||||
}>;
|
|
||||||
content?: React.ReactNode;
|
content?: React.ReactNode;
|
||||||
}
|
}
|
9
frontend/src/utils/textUtils.ts
Normal file
9
frontend/src/utils/textUtils.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Filters out emoji characters from a text string
|
||||||
|
* @param text - The input text string
|
||||||
|
* @returns The filtered text without emoji characters
|
||||||
|
*/
|
||||||
|
export const removeEmojis = (text: string): string => {
|
||||||
|
// Filter out emoji characters (Unicode ranges for emojis)
|
||||||
|
return text.replace(/[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '');
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user