บทนำ
องค์กรหลายแห่งมักต้องการใส่แบรนด์หรือปกป้องไฟล์จำนวนหลายพันไฟล์ – สัญญา, งานนำเสนอ, ใบแจ้งหนี้ – ในการดำเนินการเดียว การทำแบบนี้ด้วยมือหมายถึงต้องเปิดเอกสารแต่ละไฟล์, แทรกโลโก้หรือข้อความความลับ, แล้วบันทึกใหม่ ไม่เพียงแต่กระบวนการใช้เวลานาน, ยังทำให้เกิดข้อผิดพลาดของมนุษย์และเสี่ยงต่อ การใส่น้ำลายน้ำซ้ำ หรือไฟล์ที่พลาด
GroupDocs.Watermark for .NET แก้ปัญหาด้วย API แบบรวมที่ทำงานได้กับ PDF, DOCX, PPTX, XLSX และรูปแบบภาพทั่วไป ตัวอย่างโปรเจกต์มาพร้อมกับ สี่ประเภทเอกสาร (DOCX, PDF, XLSX, PPTX) ทำให้ทุกโหมดของ pipeline ทำงานกับรูปแบบที่ใช้ในโลกจริง ในบทแนะนำนี้เราจะเดินผ่าน pipeline การใส่น้ำลายน้ำแบบแบตช์ ที่สมบูรณ์ ซึ่ง:
- โหลดไลเซนส์ (หรือถอยกลับเป็นโหมดประเมินผล)
- สแกนโฟลเดอร์และกรองเฉพาะรูปแบบที่ไลบรารีรองรับ
- ใส่น้ำลายน้ำ ข้อความ แบบกระเบื้องลงในทุกเอกสาร
- ใส่น้ำลายน้ำ โลโก้ แบบกระเบื้องพร้อมความทึบและการหมุนที่กำหนดเอง
- เพิ่มน้ำลายน้ำ เฉพาะเมื่อยังไม่มี (การประมวลผลแบบ idempotent)
- ค้นหาและแทนที่ โลโก้บริษัทที่ล้าสมัยด้วยโลโก้ใหม่
เมื่อทำครบคุณจะได้โซลูชันที่พร้อมรันที่สามารถนำไปใส่ในโปรเจกต์ .NET ใดก็ได้
ทำไมการใส่น้ำลายน้ำแบบแบตช์จึงสำคัญ
- ความสามารถขยาย – ประมวลผลหลายสิบหรือหลายพันไฟล์ด้วยลูปเดียว
- ความสอดคล้อง – รูปแบบภาพเดียวกันถูกนำไปใช้กับทุกเอกสาร, ขจัดการเบี่ยงเบนของแบรนด์
- ความปลอดภัย – ลอจิกแบบ idempotent ป้องกันการใส่น้ำลายน้ำซ้ำเมื่อ pipeline ถูกเรียกใช้ซ้ำ
- การเตรียมพร้อมในอนาคต – โค้ดการแทนที่โลโก้ทำให้คุณสามารถเปลี่ยนแบรนด์ใหม่ได้โดยไม่ต้องแก้ไขไฟล์แต่ละไฟล์ด้วยตนเอง
ข้อกำหนดเบื้องต้น
- .NET 6.0 หรือใหม่กว่า
- แพคเกจ GroupDocs.Watermark จาก NuGet (
dotnet add package GroupDocs.Watermark) - ไฟล์ไลเซนส์ (ชั่วคราวหรือถาวร) ตัวอย่างทำงานในโหมดประเมินผลหากไม่มีไฟล์
- โฟลเดอร์สองโฟลเดอร์บนดิสก์:
InputFolder– มีเอกสารต้นฉบับOutputFolder– ที่จะบันทึกสำเนาที่ใส่น้ำลายน้ำแล้ว
ขั้นตอนที่ 1 – โหลดไลเซนส์
ไลบรารีต้องการไลเซนส์เพื่อทำงานโดยไม่มีข้อจำกัดของโหมดประเมินผล โค้ดสั้นด้านล่างพยายามโหลดไฟล์ไลเซนส์และถอยกลับอย่างเงียบหากไม่พบไฟล์
try
{
var license = new License();
license.SetLicense(LicensePath);
Console.WriteLine("License applied successfully.");
}
catch
{
Console.WriteLine("Warning: License not found. Running in evaluation mode.");
}
ประเด็นสำคัญ: ตัวแปร LicensePath ควรชี้ไปที่ไฟล์ .lic ของคุณ หากไฟล์หายโค้ดจะดำเนินต่อ ซึ่งเป็นประโยชน์สำหรับการทดสอบอย่างรวดเร็ว
ขั้นตอนที่ 2 – ค้นหาไฟล์ที่รองรับ
GroupDocs.Watermark สามารถประมวลผลได้เฉพาะส่วนขยายที่กำหนด ตัวช่วยด้านล่างสแกนโฟลเดอร์, สร้างชุดแฮชของส่วนขยายที่รองรับผ่าน FileType.GetSupportedFileTypes(), แล้วคืนค่าเฉพาะไฟล์ที่ตรงกัน
if (!Directory.Exists(folderPath))
{
Console.WriteLine($"Folder not found: {folderPath}");
return new List<string>();
}
var supportedExtensions = FileType.GetSupportedFileTypes()
.Select(ft => ft.Extension.ToLowerInvariant())
.ToHashSet();
var supportedFiles = Directory.GetFiles(folderPath, "*.*", SearchOption.AllDirectories)
.Where(f => supportedExtensions.Contains(Path.GetExtension(f).ToLowerInvariant()))
.ToList();
Console.WriteLine($"Found {supportedFiles.Count} supported file(s) in '{folderPath}'.");
return supportedFiles;
ประเด็นสำคัญ: เมธอดนี้รับประกันว่าลูปการใส่น้ำลายน้ำต่อไปจะไม่เจอรูปแบบที่ไม่รองรับ, ซึ่งจะทำให้เกิดข้อยกเว้นในเวลารันได้
ขั้นตอนที่ 3 – ใส่น้ำลายน้ำข้อความแบบกระเบื้อง
โค้ดต่อไปนี้สร้างน้ำลายน้ำ “CONFIDENTIAL” สีแดง, มีความทึบ‑ครึ่ง, หมุน ‑30°, และกระเบื้องทั่วทุกหน้าโดยใช้ TileOptions
Directory.CreateDirectory(outputFolder);
int processed = 0, failed = 0;
foreach (var filePath in files)
{
try
{
using var watermarker = new Watermarker(filePath);
var watermark = new TextWatermark(watermarkText,
new Font("Arial", 19, FontStyle.Bold))
{
ForegroundColor = Color.Red,
Opacity = 0.3,
RotateAngle = -30,
TileOptions = new TileOptions
{
LineSpacing = new MeasureValue
{
MeasureType = TileMeasureType.Percent,
Value = 12
},
WatermarkSpacing = new MeasureValue
{
MeasureType = TileMeasureType.Percent,
Value = 10
}
}
};
watermarker.Add(watermark);
var outPath = Path.Combine(outputFolder, Path.GetFileName(filePath));
watermarker.Save(outPath);
processed++;
Console.WriteLine($"[OK] {Path.GetFileName(filePath)}");
}
catch (Exception ex)
{
failed++;
Console.WriteLine($"[FAIL] {Path.GetFileName(filePath)}: {ex.Message}");
}
}
Console.WriteLine($"Text watermark: {processed} processed, {failed} failed");
ประเด็นสำคัญ:
TileOptionsสร้างลายแบบอิฐ, ทำให้น้ำลายน้ำยากต่อการลบโดยไม่กระทบเนื้อหาเดิม- โค้ดเดียวกันทำงานกับ PDF, Word, สเปรดชีตและภาพเพราะ
Watermarkerแอบสตรักต์รูปแบบไฟล์
ขั้นตอนที่ 4 – ใส่น้ำลายน้ำโลโก้แบบกระเบื้อง
หากคุณต้องการเครื่องหมายแบรนด์แบบภาพ, แทนที่น้ำลายน้ำข้อความด้วยรูปภาพ โค้ดด้านล่างตรวจสอบว่าไฟล์โลโก้มีอยู่, แล้วกระเบื้องด้วยความทึบ 40 % และหมุน ‑30°
if (!File.Exists(logoPath))
{
Console.WriteLine($"Logo not found: {logoPath}. Skipping image mode.");
return;
}
Directory.CreateDirectory(outputFolder);
int processed = 0, failed = 0;
foreach (var filePath in files)
{
try
{
using var watermarker = new Watermarker(filePath);
using var watermark = new ImageWatermark(logoPath)
{
Opacity = 0.4,
RotateAngle = -30,
TileOptions = new TileOptions
{
LineSpacing = new MeasureValue
{
MeasureType = TileMeasureType.Percent,
Value = 15
},
WatermarkSpacing = new MeasureValue
{
MeasureType = TileMeasureType.Percent,
Value = 12
}
}
};
watermarker.Add(watermark);
var outPath = Path.Combine(outputFolder, Path.GetFileName(filePath));
watermarker.Save(outPath);
processed++;
Console.WriteLine($"[OK] {Path.GetFileName(filePath)} - logo applied");
}
catch (Exception ex)
{
failed++;
Console.WriteLine($"[FAIL] {Path.GetFileName(filePath)}: {ex.Message}");
}
}
Console.WriteLine($"Logo watermark: {processed} processed, {failed} failed");
ประเด็นสำคัญ:
- ลอจิก
TileOptionsเดียวกับข้อความทำงานกับภาพ, ให้ลุคที่สอดคล้องกันในทุกหน้า Opacityทำให้เนื้อหาต้นฉบับยังอ่านได้ในขณะที่แบรนด์ยังมองเห็นได้
ขั้นตอนที่ 5 – การใส่น้ำลายน้ำแบบ Idempotent (ข้ามที่มีอยู่แล้ว)
การรัน pipeline หลายครั้งไม่ควรทำให้น้ำลายน้ำซ้อนกัน โค้ดสั้นนี้ค้นหา ข้อความ น้ำลายน้ำที่ตรงกันก่อนจะเพิ่มใหม่
Directory.CreateDirectory(outputFolder);
int applied = 0, skipped = 0, failed = 0;
foreach (var filePath in files)
{
try
{
using var watermarker = new Watermarker(filePath);
var criteria = new TextSearchCriteria(watermarkText, false);
var existing = watermarker.Search(criteria);
if (existing.Count > 0)
{
skipped++;
Console.WriteLine($"[SKIP] {Path.GetFileName(filePath)} – already contains watermark");
continue;
}
var watermark = new TextWatermark(watermarkText,
new Font("Arial", 19, FontStyle.Bold))
{
ForegroundColor = Color.Red,
Opacity = 0.3,
RotateAngle = -30,
TileOptions = new TileOptions
{
LineSpacing = new MeasureValue
{
MeasureType = TileMeasureType.Percent,
Value = 12
},
WatermarkSpacing = new MeasureValue
{
MeasureType = TileMeasureType.Percent,
Value = 10
}
}
};
watermarker.Add(watermark);
var outPath = Path.Combine(outputFolder, Path.GetFileName(filePath));
watermarker.Save(outPath);
applied++;
Console.WriteLine($"[OK] {Path.GetFileName(filePath)} – watermark applied");
}
catch (Exception ex)
{
failed++;
Console.WriteLine($"[FAIL] {Path.GetFileName(filePath)}: {ex.Message}");
}
}
Console.WriteLine($"Smart batch: {applied} applied, {skipped} skipped, {failed} failed");
ประเด็นสำคัญ: TextSearchCriteria ที่มี false สำหรับความไวต่อกรณีทำให้เราข้ามเอกสารที่มี ข้อความ น้ำลายน้ำที่ตรงกับที่เราตั้งใจจะเพิ่มเท่านั้น
ขั้นตอนที่ 6 – แทนที่โลโก้ที่ล้าสมัยทั่วโฟลเดอร์
เมื่อบริษัททำการรีแบรนด์, คุณอาจต้องสลับโลโก้เก่าทุกอันด้วยโลโก้ใหม่ โค้ดนี้รวมสองกลยุทธ์การค้นหาภาพ – DCT‑hash สำหรับความแม่นยำและ colour‑histogram สำหรับความยืดหยุ่น – แล้วเขียนทับข้อมูลภาพของแต่ละผลลัพธ์
if (!File.Exists(oldLogoPath) || !File.Exists(newLogoPath))
{
Console.WriteLine("Old or new logo file missing – aborting replacement.");
return;
}
Directory.CreateDirectory(outputFolder);
byte[] newLogoData = File.ReadAllBytes(newLogoPath);
int replaced = 0, notFound = 0;
var settings = new WatermarkerSettings
{
SearchableObjects = new SearchableObjects
{
PdfSearchableObjects = PdfSearchableObjects.All
}
};
foreach (var filePath in files)
{
try
{
using var watermarker = new Watermarker(filePath, settings);
var dct = new ImageDctHashSearchCriteria(oldLogoPath) { MaxDifference = 0.4 };
var hist = new ImageColorHistogramSearchCriteria(oldLogoPath) { MaxDifference = 0.5 };
var criteria = dct.Or(hist);
var found = watermarker.Search(criteria);
if (found.Count == 0)
{
notFound++;
Console.WriteLine($"[--] {Path.GetFileName(filePath)} – old logo not found");
continue;
}
foreach (PossibleWatermark wm in found)
{
try
{
wm.ImageData = newLogoData;
}
catch
{
// Some watermark types cannot be overwritten – ignore.
}
}
var outPath = Path.Combine(outputFolder, Path.GetFileName(filePath));
watermarker.Save(outPath);
replaced++;
Console.WriteLine($"[OK] {Path.GetFileName(filePath)} – {found.Count} logo(s) replaced");
}
catch (Exception ex)
{
Console.WriteLine($"[FAIL] {Path.GetFileName(filePath)}: {ex.Message}");
}
}
Console.WriteLine($"Logo replacement: {replaced} updated, {notFound} without old logo");
ประเด็นสำคัญ:
WatermarkerSettingsพร้อมPdfSearchableObjects.Allทำให้การค้นหาเห็นโลโก้ที่เก็บเป็น PDF artifacts- การรวมเกณฑ์ DCT‑hash และ colour‑histogram ช่วยจับโลโก้เวกเตอร์ (Office) และเวอร์ชันเรสเตอร์ (PDF) ทั้งสองแบบ
แนวทางปฏิบัติที่ดีที่สุด & เคล็ดลับ
- สร้างโฟลเดอร์ผลลัพธ์เพียงครั้งเดียว (
Directory.CreateDirectory) – เมธอดนี้เป็น idempotent และช่วยหลีกเลี่ยง race conditions - บันทึกความคืบหน้า – การแสดงผลในคอนโซลของแต่ละขั้นตอนทำให้เห็นไฟล์ที่สำเร็จหรือไม่สำเร็จได้ง่าย
- ปรับ
OpacityและRotateAngleตามแนวทางแบรนด์; ค่าอยู่ระหว่าง 0.3–0.5 มักพอให้มองเห็นแต่ไม่รบกวน - ใช้ smart batch แบบ idempotent สำหรับงานที่ทำซ้ำ (เช่น การอัปเดตแบรนด์ทุกคืน)
- ทดสอบการแทนที่โลโก้บนตัวอย่างขนาดเล็ก ก่อนรันทั่วทั้งคลังเพื่อให้แน่ใจว่าเกณฑ์การค้นหาถูกตั้งค่าอย่างเหมาะสม
แก้ไขปัญหาที่พบบ่อย
| อาการ | สาเหตุที่เป็นไปได้ | วิธีแก้ |
|---|---|---|
| ไม่พบไฟล์ใดถูกประมวลผล | ScanFolderForSupportedFiles คืนรายการว่าง |
ตรวจสอบเส้นทาง InputFolder และให้แน่ใจว่าโฟลเดอร์มีรูปแบบที่รองรับ (PDF, DOCX, PPTX, XLSX, PNG, JPG ฯลฯ) |
| น้ำลายน้ำไม่ปรากฏ | ความทึบตั้งค่าต่ำเกินไปหรือสีผสมกับพื้นหลัง | เพิ่ม Opacity (เช่น 0.5) หรือเปลี่ยน ForegroundColor เป็นสีที่ตัดกัน |
| ไม่พบโลโก้ PDF ระหว่างการแทนที่ | โลโก้ถูกเพิ่มเป็น draw operators ใน content‑stream (ไม่สามารถค้นหาได้) | เมื่อใส่โลโก้, ใช้ PdfArtifactWatermarkOptions เพื่อทำให้เป็น artifacts ที่ค้นหาได้ |
เกิดข้อยกเว้น System.Drawing.Common บน Linux |
ขาดไลบรารี GDI+ เนทีฟ | ติดตั้ง libgdiplus บนเครื่อง Linux หรือเปิดใช้งาน Unix support ในไฟล์ .csproj (<RuntimeHostConfigurationOption Include="System.Drawing.EnableUnixSupport" Value="true" />). |
สรุป
คุณมี pipeline ที่สมบูรณ์และพร้อมใช้งานในระดับผลิต ที่สามารถ:
- ใบ้ไลเซนส์ให้กับไลบรารี
- ตรวจจับเอกสารที่รองรับโดยอัตโนมัติ
- ใส่น้ำลายน้ำข้อความหรือโลโก้แบบกระเบื้อง
- ทำงานอย่างปลอดภัยหลายครั้งโดยไม่สร้างซ้ำ
- แทนที่โลโก้บริษัทเก่าทั่วโฟลเดอร์ทั้งหมด
บล็อกสร้างเหล่านี้สามารถผสมผสานกันเพื่อให้เข้ากับกระบวนการแบรนด์หรือการปกป้องเอกสารใด ๆ ใน .NET