إضافة العلامة المائية للصور وملفات PDF
أصبحت العلامة المائية Watermark جزءًا مهمًا من كثير من التطبيقات التي تتعامل مع الصور والمستندات الرقمية. عندما تقوم شركة بنشر صور منتجاتها على موقع إلكتروني، أو عندما يرفع المستخدمون ملفاتهم إلى منصة لإدارة المستندات، أو عندما تُصدر فواتير وتقارير وملفات PDF خاصة بالعملاء، فإن إضافة شعار أو اسم أو عبارة تعريفية إلى المحتوى قد تكون خطوة عملية جدًا لحماية الهوية البصرية للمحتوى، وتقليل احتمالية إعادة استخدامه بدون الإشارة إلى المصدر، وتمييز الملفات التي تم إنشاؤها بواسطة نظام معين. الفكرة تبدو بسيطة جدًا في ظاهرها: لدينا صورة أو ملف PDF، ونريد أن نضع فوقه شعارًا أو نصًا بدرجة شفافية محددة وفي موضع معين. لكن بمجرد الدخول إلى التطبيق الحقيقي، تظهر تفاصيل كثيرة مثل اختلاف أبعاد الصور، اختلاف اتجاه الصفحات، الحفاظ على نسبة الشعار، دعم ملفات PNG الشفافة، التعامل مع النصوص الدوارة، المحافظة على جودة الصورة، تقليل حجم الملفات الناتجة، ومعالجة ملفات PDF متعددة الصفحات بكفاءة.
في Python يمكن تنفيذ هذا النوع من المهام بطريقة نظيفة وقابلة للتوسعة. بالنسبة للصور، تعتبر Pillow من أكثر المكتبات العملية لهذا الغرض، وهي تتعامل مع الصور النقطية وتدعم نمط RGBA الذي يتكون من الأحمر والأخضر والأزرق وقناة Alpha الخاصة بالشفافية. هذا مهم جدًا لأن العلامة المائية غالبًا لا نريدها أن تكون عنصرًا صلبًا يغطي الصورة، بل عنصرًا شفافًا يظهر فوق المحتوى من دون أن يفسده. أما بالنسبة إلى PDF، فإن PyMuPDF يوفر API مباشرة للتعامل مع الصفحات وإدراج الصور والنصوص فوق المحتوى أو خلفه، وتوضح وثائقه صراحةً مثالًا بسيطًا لإضافة صورة watermark إلى كل صفحة عبر page.insert_image().
المهم هنا أن نتعامل مع العلامة المائية باعتبارها جزءًا من معالجة المحتوى وليس مجرد سطر برمجي منفصل. جودة النتيجة النهائية تعتمد على كيفية إنشاء طبقة الـ watermark نفسها، وكيفية حساب أبعادها وموقعها، وكيفية اختيار درجة الشفافية، وكيفية حفظ الملف، وكيفية التعامل مع الحالات المختلفة. في هذا المقال سنبدأ من الأساس، ثم نبني حلولًا عملية قابلة لإعادة الاستخدام، ثم نطوّرها إلى أداة كاملة يمكن استخدامها من خلال سطر الأوامر أو دمجها داخل Django أو Flask أو FastAPI أو أي تطبيق Python آخر.
ما المقصود بالعلامة المائية Watermark؟
العلامة المائية هي عنصر بصري يتم دمجه مع المحتوى الأساسي بحيث يصبح جزءًا من الصورة أو المستند النهائي. يمكن أن تكون العلامة المائية عبارة عن نص مثل اسم الموقع أو اسم الشركة، أو شعارًا بصيغة PNG، أو رمزًا، أو عنوان موقع إلكتروني، أو معرفًا فريدًا للمستخدم، أو حتى نصًا قطريًا كبيرًا يمتد عبر الصفحة. في المشاريع الواقعية لا يوجد شكل واحد صحيح للعلامة المائية. الطريقة المناسبة تعتمد على الغرض.
قد تستخدم علامة مائية صغيرة في زاوية الصورة عندما يكون هدفك الأساسي إبراز هوية العلامة التجارية مع المحافظة على شكل الصورة. وقد تستخدم علامة مائية كبيرة وشفافة في الوسط عندما تريد جعل إعادة استخدام المحتوى التجاري أكثر صعوبة. وفي المستندات قد تستخدم عبارة مثل CONFIDENTIAL أو DRAFT أو اسم العميل أو رقم الطلب. في هذه الحالة لا تكون العلامة المائية مجرد شعار، بل تصبح عنصرًا وظيفيًا داخل المستند.
ومن المهم جدًا فهم أن العلامة المائية ليست نظام حماية مطلقًا. إذا كانت العلامة المائية مجرد عنصر مرئي داخل صورة، فمن الممكن نظريًا قص الصورة أو إعادة تحريرها أو استخدام تقنيات إزالة العناصر. لكنها مع ذلك تضيف طبقة واضحة من الهوية والتتبع البصري، وهذا له قيمة كبيرة في حالات كثيرة. في المستندات الداخلية مثلًا، وجود كلمة CONFIDENTIAL في الخلفية يمكن أن يمنع مشاركة الوثيقة بالخطأ أو يجعل طبيعة الملف واضحة بمجرد فتحه.
هناك أيضًا فرق بين العلامة المائية المرئية والعلامة المائية غير المرئية أو علامات البيانات المخفية. في هذا المقال سنركز على العلامات المائية المرئية باستخدام النصوص والصور، لأنها أسهل في الفهم والتنفيذ، وهي أيضًا النوع الذي يمكن تطبيقه بسهولة على الصور وPDF باستخدام Python.
لماذا نستخدم Python لإضافة Watermark؟
Python مناسبة جدًا لهذه المهمة لأنها توفر مجموعة قوية من المكتبات المتخصصة في معالجة الوسائط والمستندات. بدلًا من الاعتماد على برامج رسومية أو تنفيذ العملية يدويًا على كل ملف، يمكننا كتابة سكربت واحد يعالج مئات أو آلاف الملفات وفق قواعد محددة.
تخيل مثلًا متجرًا إلكترونيًا يستقبل صور المنتجات من الموظفين. كل صورة يتم رفعها إلى النظام يجب أن تحصل تلقائيًا على شعار المتجر في الركن السفلي الأيسر. تنفيذ هذا يدويًا لا معنى له، لكن باستخدام Python يمكن أن تتم العملية بمجرد رفع الصورة. السيناريو نفسه ينطبق على منصة مستندات تقوم بإنشاء تقارير PDF لكل مستخدم، ثم تريد وضع اسم العميل أو شعار المؤسسة على جميع الصفحات قبل إتاحة الملف للتنزيل.
الجميل أيضًا أن Python لا تجبرك على حل واحد. يمكنك إنشاء watermark بالصورة، أو إنشاء watermark بالنص، أو الجمع بين الاثنين، أو جعل الموقع والشفافية والأبعاد متغيرة بحسب كل ملف. ويمكنك أيضًا إضافة API بحيث يرسل العميل صورة أو PDF إلى الخادم وتعود إليه نسخة تحمل العلامة المائية.
من الناحية المعمارية يمكن تقسيم الحل إلى ثلاث طبقات بسيطة:
طبقة تحميل الملف والتحقق منه.
طبقة إنشاء أو تجهيز العلامة المائية.
طبقة دمج العلامة المائية مع الملف ثم حفظ النتيجة.
هذا الفصل بين المسؤوليات يجعل المشروع أسهل في الاختبار والتطوير. بدلًا من كتابة دالة ضخمة تعالج كل شيء مرة واحدة، سننشئ مجموعة من الدوال الصغيرة ثم نربطها معًا.
تثبيت المكتبات المطلوبة
سنحتاج بشكل أساسي إلى Pillow لمعالجة الصور، وPyMuPDF لمعالجة PDF. توثيق PyMuPDF الحالي يوصي باستخدام pip install --upgrade pymupdf، كما أن الإصدارات الحديثة تستخدم import pymupdf بدل الاعتماد على اسم الاستيراد التاريخي fitz.
ابدأ بإنشاء بيئة افتراضية:
python -m venv venvفي Windows:
venv\Scripts\activateوفي Linux أو macOS:
source venv/bin/activateثم ثبّت المكتبات:
python -m pip install --upgrade pip
pip install pillow pymupdfيمكنك التأكد من التثبيت:
python -c "import PIL; print(PIL.__version__)"
python -c "import pymupdf; print(pymupdf.__doc__)"ومن الجيد أثناء تطوير مشروع حقيقي أن تثبت الإصدارات التي اختبرتها بدل ترك بيئة الإنتاج تعتمد على أحدث نسخة في كل عملية نشر. مثال:
Pillow>=12,<13
PyMuPDF>=1.28,<1.29الأرقام السابقة مثال على سياسة تثبيت محافظة وليست شرطًا مطلقًا. قبل الإنتاج، من الأفضل مراجعة الإصدارات المتاحة في بيئتك واختبار الكود معها.
تجهيز بنية المشروع
لننشئ مشروعًا بسيطًا:
watermark_project/
│
├── input/
│ ├── photo.jpg
│ └── document.pdf
│
├── watermark/
│ └── logo.png
│
├── output/
│
├── image_watermark.py
├── pdf_watermark.py
└── main.pyوجود مجلدات منفصلة يجعل التجربة واضحة. نضع الصور الأصلية داخل input، والـ watermark داخل watermark، ثم نحفظ النتائج داخل output.
يمكن أن يكون ملف logo.png شعارًا شفافًا بخلفية Alpha. وهذه نقطة مهمة للغاية، لأن PNG الشفاف عادةً مناسب أكثر للعلامات المائية من JPG الذي لا يحتفظ بقناة Alpha بالطريقة نفسها.
فهم الشفافية Alpha قبل كتابة الكود
من أكثر الأخطاء شيوعًا عند التعامل مع watermark هو الاعتقاد بأن كتابة قيمة مثل:
fill=(255, 255, 255, 50)تعني بالضرورة أن العنصر سيظهر شفافًا عند الرسم مباشرة على صورة RGB. الواقع أن التعامل مع Alpha يعتمد على وضع الصورة. Pillow يوضح أن قيم قناة Alpha لها معنى في نمط RGBA، بينما الرسم المباشر على صورة RGB لا يعطي نفس النتيجة المتوقعة للشفافية. لذلك من الأفضل إنشاء طبقة منفصلة بصيغة RGBA ثم دمجها مع الصورة الأساسية.
الفكرة الأساسية هي:
الصورة الأصلية
+
طبقة شفافة RGBA
|
v
Alpha Composite
|
v
الصورة النهائيةهذه الطريقة تجعل التحكم بالـ watermark أكثر مرونة. يمكنك إنشاء طبقة بنفس أبعاد الصورة الأصلية، ثم رسم النص أو وضع الشعار عليها، وبعد ذلك دمجها مع الصورة.
إضافة نص Watermark إلى صورة
لنبدأ بأسهل مثال ممكن.
from PIL import Image, ImageDraw, ImageFont
image = Image.open("input/photo.jpg").convert("RGBA")
overlay = Image.new("RGBA", image.size, (255, 255, 255, 0))
draw = ImageDraw.Draw(overlay)
font = ImageFont.truetype("arial.ttf", 48)
draw.text(
(40, 40),
"My Website",
font=font,
fill=(255, 255, 255, 100)
)
result = Image.alpha_composite(image, overlay)
result.convert("RGB").save("output/photo_watermarked.jpg", quality=90)هنا قمنا بإنشاء طبقة شفافة:
overlay = Image.new("RGBA", image.size, (255, 255, 255, 0))القيمة الأخيرة 0 تعني أن الخلفية شفافة بالكامل.
بعد ذلك رسمنا النص على الطبقة:
fill=(255, 255, 255, 100)القيمة 100 هي مستوى Alpha للنص، حيث القيمة الأعلى تعني ظهورًا أكثر وضوحًا.
ثم دمجنا الطبقة:
Image.alpha_composite(image, overlay)وهو مفهوم أساسي في معالجة watermark بالصور. توثيق Pillow يوضح أن alpha_composite() مخصص لدمج صور مع قنوات شفافية فوق بعضها.
جعل موضع النص في الزاوية السفلية
تثبيت النص عند (40, 40) ليس عمليًا دائمًا، لأننا قد نتعامل مع صورة صغيرة جدًا أو صورة ضخمة جدًا. الأفضل أن نحسب الموقع اعتمادًا على أبعاد الصورة.
from PIL import Image, ImageDraw, ImageFont
image = Image.open("input/photo.jpg").convert("RGBA")
overlay = Image.new("RGBA", image.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
font = ImageFont.truetype("arial.ttf", 48)
text = "My Website"
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
margin = 40
x = image.width - text_width - margin
y = image.height - text_height - margin
draw.text(
(x, y),
text,
font=font,
fill=(255, 255, 255, 100)
)
result = Image.alpha_composite(image, overlay)
result.convert("RGB").save(
"output/photo_watermarked.jpg",
quality=90
)استخدام textbbox() مهم لأن أبعاد النص لا يجب أن تُحسب بطريقة بدائية تعتمد على عدد الأحرف فقط. Pillow يوفر دوال لحساب حدود النص وتقدير المساحة اللازمة لعرضه.
هذه نقطة صغيرة لكنها تمثل الفرق بين سكربت تجريبي وأداة يمكن استخدامها داخل نظام حقيقي.
إنشاء دالة عامة لإضافة Text Watermark
بدل تكرار الكود، سننشئ دالة قابلة لإعادة الاستخدام:
from PIL import Image, ImageDraw, ImageFont
def add_text_watermark(
image_path,
output_path,
text,
font_path,
font_size=48,
opacity=100,
margin=40,
):
image = Image.open(image_path).convert("RGBA")
overlay = Image.new("RGBA", image.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
font = ImageFont.truetype(font_path, font_size)
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = image.width - text_width - margin
y = image.height - text_height - margin
draw.text(
(x, y),
text,
font=font,
fill=(255, 255, 255, opacity),
)
result = Image.alpha_composite(image, overlay)
result = result.convert("RGB")
result.save(output_path, quality=90)
add_text_watermark(
"input/photo.jpg",
"output/photo_watermarked.jpg",
"example.com",
"arial.ttf",
)الآن أصبحت العملية قابلة لإعادة الاستخدام. يمكنك استدعاء الدالة داخل Django view أو job في Celery أو endpoint في FastAPI أو حتى سكربت batch.
دعم مواقع متعددة للعلامة المائية
في المشروع الحقيقي قد تحتاج إلى دعم:
top-left
top-center
top-right
center
bottom-left
bottom-center
bottom-rightيمكن بناء دالة واحدة لذلك:
def calculate_position(
image_size,
item_size,
position,
margin=40,
):
image_width, image_height = image_size
item_width, item_height = item_size
if position == "top-left":
return margin, margin
if position == "top-center":
return (
(image_width - item_width) // 2,
margin
)
if position == "top-right":
return (
image_width - item_width - margin,
margin
)
if position == "center":
return (
(image_width - item_width) // 2,
(image_height - item_height) // 2
)
if position == "bottom-left":
return (
margin,
image_height - item_height - margin
)
if position == "bottom-center":
return (
(image_width - item_width) // 2,
image_height - item_height - margin
)
if position == "bottom-right":
return (
image_width - item_width - margin,
image_height - item_height - margin
)
raise ValueError(f"Unsupported position: {position}")بهذه الطريقة يصبح تحديد الموقع قيمة configuration بدل أن يكون منطقًا ثابتًا داخل كل دالة.
إضافة شعار Logo Watermark إلى صورة
في كثير من الحالات لن يكون النص هو الخيار الأفضل. الشركات عادةً تمتلك شعارًا بصيغة PNG بخلفية شفافة.
لنكتب دالة لإضافة الشعار:
from PIL import Image
def add_logo_watermark(
image_path,
logo_path,
output_path,
position="bottom-right",
scale=0.2,
opacity=160,
margin=30,
):
image = Image.open(image_path).convert("RGBA")
logo = Image.open(logo_path).convert("RGBA")
target_width = int(image.width * scale)
ratio = target_width / logo.width
target_height = int(logo.height * ratio)
logo = logo.resize(
(target_width, target_height),
Image.Resampling.LANCZOS
)
alpha = logo.getchannel("A")
alpha = alpha.point(lambda value: value * opacity // 255)
logo.putalpha(alpha)
position_xy = calculate_position(
image.size,
logo.size,
position,
margin
)
overlay = Image.new(
"RGBA",
image.size,
(0, 0, 0, 0)
)
overlay.alpha_composite(
logo,
dest=position_xy
)
result = Image.alpha_composite(
image,
overlay
)
result.convert("RGB").save(
output_path,
quality=90,
optimize=True
)يمكن تشغيلها هكذا:
add_logo_watermark(
image_path="input/photo.jpg",
logo_path="watermark/logo.png",
output_path="output/photo_watermarked.jpg",
position="bottom-right",
scale=0.18,
opacity=150,
)هنا لدينا مفهوم مهم: العلامة المائية يتم تغيير حجمها بناءً على عرض الصورة وليس بمقاس ثابت. إذا كانت الصورة 500 بكسل عرضًا، لن نريد شعارًا عرضه 700 بكسل. وإذا كانت الصورة 5000 بكسل، فقد يبدو شعار بعرض 100 بكسل صغيرًا جدًا.
لماذا يجب الحفاظ على Aspect Ratio؟
عند تغيير حجم الشعار، من الخطأ تغيير العرض والارتفاع بشكل مستقل:
logo = logo.resize((500, 100))إلا إذا كنت تعرف أن الشعار أصلًا يحمل هذه النسبة.
الأفضل هو حساب أحد البعدين ثم اشتقاق الآخر من النسبة الأصلية:
ratio = target_width / logo.width
target_height = int(logo.height * ratio)وبذلك لا يتحول الشعار إلى شكل ممدود أو مضغوط. PyMuPDF أيضًا يجعل الحفاظ على نسبة الصورة هو السلوك الافتراضي في insert_image() ويمكن التحكم بذلك عبر keep_proportion.
التحكم الحقيقي في شفافية الشعار
قد تجد أن الشعار قوي جدًا. يمكننا تعديل Alpha الخاص به قبل الدمج.
alpha = logo.getchannel("A")نحصل هنا على قناة Alpha الموجودة أصلًا.
ثم:
alpha = alpha.point(
lambda value: value * 150 // 255
)بعد ذلك نعيد القناة:
logo.putalpha(alpha)الفائدة في هذا الأسلوب أننا لا نحتاج إلى تغيير RGB نفسه. نحتفظ بألوان الشعار ونعدل فقط مدى ظهوره.
يمكن إنشاء دالة صغيرة:
def change_opacity(image, opacity):
image = image.convert("RGBA")
alpha = image.getchannel("A")
alpha = alpha.point(
lambda value: value * opacity // 255
)
image.putalpha(alpha)
return imageثم:
logo = change_opacity(logo, 120)وهذه الطريقة مفيدة جدًا عندما تريد إعطاء نفس الشعار مستويات مختلفة من الوضوح حسب نوع الصورة أو المستند.
إضافة Watermark قطري Diagonal Watermark إلى صورة
العلامة المائية القطرية شائعة خصوصًا في الصور التي تحتاج إلى إظهار الملكية أو الحالة.
يمكننا إنشاء طبقة منفصلة، كتابة النص عليها، ثم تدوير الطبقة.
from PIL import Image, ImageDraw, ImageFont
def create_rotated_text_watermark(
text,
font_path,
font_size,
opacity=80,
angle=35,
):
font = ImageFont.truetype(
font_path,
font_size
)
temp = Image.new(
"RGBA",
(2000, 500),
(0, 0, 0, 0)
)
draw = ImageDraw.Draw(temp)
bbox = draw.textbbox(
(0, 0),
text,
font=font
)
width = bbox[2] - bbox[0]
height = bbox[3] - bbox[1]
draw.text(
(50, 50),
text,
font=font,
fill=(255, 255, 255, opacity)
)
temp = temp.crop((
0,
0,
width + 100,
height + 100
))
return temp.rotate(
angle,
expand=True,
resample=Image.Resampling.BICUBIC
)ثم:
image = Image.open(
"input/photo.jpg"
).convert("RGBA")
watermark = create_rotated_text_watermark(
text="CONFIDENTIAL",
font_path="arial.ttf",
font_size=90,
opacity=70,
angle=35,
)
x = (
image.width - watermark.width
) // 2
y = (
image.height - watermark.height
) // 2
image.alpha_composite(
watermark,
(x, y)
)
image.convert("RGB").save(
"output/confidential.jpg",
quality=90
)إذا كنت ستستخدم هذه الطريقة على صور كبيرة جدًا، فمن الأفضل التفكير في حجم الطبقة قبل التدوير حتى لا تستهلك ذاكرة كبيرة بلا داعٍ.
الجمع بين الشعار والنص
لا يوجد ما يمنع من وضع الشعار والنص معًا.
مثلًا:
[LOGO]
example.comيمكن بناء watermark مركبة:
from PIL import Image, ImageDraw, ImageFont
def create_logo_and_text_watermark(
logo_path,
text,
font_path,
font_size=40,
opacity=130,
):
logo = Image.open(
logo_path
).convert("RGBA")
logo.thumbnail(
(300, 300),
Image.Resampling.LANCZOS
)
font = ImageFont.truetype(
font_path,
font_size
)
dummy = Image.new(
"RGBA",
(10, 10),
(0, 0, 0, 0)
)
draw = ImageDraw.Draw(dummy)
bbox = draw.textbbox(
(0, 0),
text,
font=font
)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
padding = 20
width = max(
logo.width,
text_width
) + padding * 2
height = (
logo.height
+ text_height
+ padding * 3
)
watermark = Image.new(
"RGBA",
(width, height),
(0, 0, 0, 0)
)
logo_x = (
width - logo.width
) // 2
watermark.alpha_composite(
logo,
(logo_x, padding)
)
draw = ImageDraw.Draw(watermark)
text_x = (
width - text_width
) // 2
text_y = (
padding * 2
+ logo.height
)
draw.text(
(text_x, text_y),
text,
font=font,
fill=(255, 255, 255, opacity)
)
return watermarkيمكن بعدها وضع الناتج على أي صورة.
الفائدة الحقيقية من فصل create_logo_and_text_watermark() عن add_watermark_to_image() أن إنشاء العلامة المائية أصبح مستقلًا عن المحتوى. هذه فكرة تصميمية مهمة. لو أردت غدًا إضافة إطار أو أيقونة أو رقم إصدار إلى watermark، لن تحتاج إلى إعادة كتابة منطق معالجة الصورة كله.
التعامل مع الصور ذات الاتجاه الخاطئ EXIF
بعض الصور التي تلتقط بواسطة الهواتف تحتوي على Orientation داخل بيانات EXIF بدل أن تكون البكسلات نفسها مدورة. عند فتحها ومعالجتها، قد تجد أن الصورة تظهر بشكل مختلف بحسب البرنامج الذي يعرضها.
لهذا السبب من الممارسات المفيدة استخدام:
from PIL import Image, ImageOps
image = Image.open("input/photo.jpg")
image = ImageOps.exif_transpose(image)
image = image.convert("RGBA")بهذه الطريقة يتم تصحيح الاتجاه بصريًا قبل حساب موضع العلامة المائية.
هذا مهم بشكل خاص إذا كنت تضع watermark في الركن السفلي. بدون تصحيح الاتجاه قد يصبح "الركن السفلي" الذي حسبه البرنامج مختلفًا عن الاتجاه الذي يراه المستخدم بعد عرض الصورة.
الحفاظ على جودة الصور
عند حفظ صورة JPEG:
image.save(
"output/result.jpg",
quality=90
)يمكنك اختيار جودة مناسبة حسب احتياجاتك. الجودة العالية لا تعني دائمًا أن الصورة ستكون أفضل بشكل واضح، لكنها عادةً تقلل آثار الضغط مقارنة بقيم منخفضة.
يمكن أيضًا:
image.save(
"output/result.jpg",
quality=90,
optimize=True
)لكن لا تجعل quality=100 قاعدة ثابتة لجميع الصور، لأن حجم الملف قد يكبر كثيرًا دون فائدة بصرية تستحق ذلك.
أما PNG فيمكن حفظه:
result.save(
"output/result.png",
optimize=True
)ميزة PNG المهمة هنا أنه مناسب عندما تحتاج إلى الشفافية أو عندما يكون المحتوى رسومات وشعارات ونصوص أكثر من كونه صورًا فوتوغرافية.
معالجة مجموعة صور دفعة واحدة
بعد أن أصبحت لدينا دالة واحدة، يمكن معالجة مجلد كامل:
from pathlib import Path
input_dir = Path("input")
output_dir = Path("output")
output_dir.mkdir(
exist_ok=True
)
for image_path in input_dir.iterdir():
if image_path.suffix.lower() not in {
".jpg",
".jpeg",
".png",
".webp",
}:
continue
output_path = (
output_dir
/ f"{image_path.stem}_watermarked.jpg"
)
add_text_watermark(
image_path=image_path,
output_path=output_path,
text="example.com",
font_path="arial.ttf",
font_size=48,
opacity=100,
)
print(
f"Processed: {image_path.name}"
)هنا لدينا Batch Processing بسيط. لكن في مشروع حقيقي هناك أمور إضافية تستحق التعامل معها مثل الملفات التالفة، الاستثناءات، الصور الكبيرة جدًا، والحد الأقصى لحجم الملف.
يمكن تحسين ذلك:
for image_path in input_dir.iterdir():
try:
if image_path.suffix.lower() not in {
".jpg",
".jpeg",
".png",
".webp",
}:
continue
output_path = (
output_dir
/ f"{image_path.stem}_watermarked.jpg"
)
add_text_watermark(
image_path,
output_path,
"example.com",
"arial.ttf",
)
except Exception as exc:
print(
f"Failed: {image_path.name}: {exc}"
)لكن لا تستخدم except Exception في تطبيق كبير بدون logging مناسب. الأفضل تسجيل نوع الخطأ والملف المتسبب به حتى تستطيع مراجعة المشكلة لاحقًا.
الانتقال إلى PDF
الآن نصل إلى الجزء الثاني من المقال: إضافة watermark إلى PDF.
PDF ليس مجرد صورة واحدة كبيرة. قد يحتوي على نص قابل للتحديد، صور، خطوط، رسومات متجهة، روابط، بيانات وصفية، صفحات متعددة، وغير ذلك. لذلك يجب استخدام مكتبة متخصصة في PDF.
PyMuPDF مناسب لهذا النوع من العمل. التوثيق الحالي يوضح أن فتح مستند يتم بواسطة:
import pymupdf
doc = pymupdf.open("document.pdf")كما يوضح مثال العلامة المائية الرسمي فكرة المرور على الصفحات ثم استخدام page.insert_image() لإدراج صورة watermark داخل حدود الصفحة.
أبسط Watermark بصورة PNG على PDF
لنبدأ بمثال مباشر:
import pymupdf
doc = pymupdf.open(
"input/document.pdf"
)
for page in doc:
page.insert_image(
page.rect,
filename="watermark/logo.png",
overlay=True,
)
doc.save(
"output/document_watermarked.pdf"
)
doc.close()هذا يضع الصورة داخل كامل مساحة الصفحة، ولذلك ليس مناسبًا عادةً للشعار الصغير، لكنه يوضح الفكرة.
توضح وثائق PyMuPDF أيضًا استخدام page.bound() في المثال الرسمي لتحديد مساحة الصفحة، ويمكن استخدام page.rect كذلك كحدود للصفحة.
لماذا يجب أن يكون ملف الـ watermark شفافًا؟
إذا كان logo.png يحتوي على خلفية بيضاء صلبة، فقد تحصل على مربع أبيض فوق النص الموجود في PDF. قد يكون هذا مناسبًا في حالات محددة، لكنه ليس الشكل المعتاد للعلامة المائية.
الأفضل غالبًا:
logo.png
RGBA
خلفية شفافة
شعار واضح
Alpha منخفض نسبيًايمكنك إنشاء watermark في Pillow أولًا ثم استخدامه في PyMuPDF.
إنشاء Watermark PNG مخصص للـ PDF باستخدام Pillow
من المفيد إنشاء صورة watermark منفصلة قبل إدراجها داخل PDF:
from PIL import Image, ImageDraw, ImageFont
def create_pdf_watermark_image(
output_path,
width=1200,
height=800,
text="CONFIDENTIAL",
font_path="arial.ttf",
font_size=110,
opacity=60,
):
canvas = Image.new(
"RGBA",
(width, height),
(0, 0, 0, 0)
)
draw = ImageDraw.Draw(canvas)
font = ImageFont.truetype(
font_path,
font_size
)
bbox = draw.textbbox(
(0, 0),
text,
font=font
)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = (
width - text_width
) // 2
y = (
height - text_height
) // 2
draw.text(
(x, y),
text,
font=font,
fill=(80, 80, 80, opacity)
)
canvas.save(
output_path,
format="PNG"
)ثم:
create_pdf_watermark_image(
"watermark/pdf_watermark.png"
)بعد ذلك نستعمله مع PyMuPDF.
وضع Watermark في منتصف كل صفحة PDF
يمكننا الآن تحديد مستطيل مناسب بدل استخدام الصفحة كلها:
import pymupdf
doc = pymupdf.open(
"input/document.pdf"
)
for page in doc:
rect = page.rect
watermark_width = rect.width * 0.60
watermark_height = 180
x0 = (
rect.width - watermark_width
) / 2
y0 = (
rect.height - watermark_height
) / 2
watermark_rect = pymupdf.Rect(
x0,
y0,
x0 + watermark_width,
y0 + watermark_height,
)
page.insert_image(
watermark_rect,
filename="watermark/pdf_watermark.png",
keep_proportion=True,
overlay=True,
)
doc.save(
"output/document_watermarked.pdf"
)لاحظ أننا لا نغير أبعاد الصفحة نفسها. نحن فقط نحدد المكان الذي سيظهر فيه الـ watermark.
وثائق insert_image() توضح أن keep_proportion=True يحافظ على نسبة أبعاد الصورة، وهو السلوك الافتراضي أيضًا.
تحسين أداء PDF عند وجود صفحات كثيرة
لنفترض أن لديك PDF يحتوي على 500 صفحة. لا تريد أن تتعامل مع watermark وكأنها 500 صورة منفصلة إذا كان بالإمكان إعادة استخدام نفس مورد الصورة.
توثيق PyMuPDF يشير إلى أنه من الأفضل من ناحية الأداء وحجم الملف إعادة استخدام بيانات الصورة بدل إنشاء مرجع جديد غير ضروري في كل مرة، كما أن insert_image() يعيد XREF يمكن استخدامه لإعادة إدراج الصورة.
يمكن تطبيق الفكرة هكذا:
import pymupdf
doc = pymupdf.open(
"input/document.pdf"
)
xref = 0
for page in doc:
if xref == 0:
xref = page.insert_image(
page.rect,
filename="watermark/logo.png",
overlay=True,
)
else:
page.insert_image(
page.rect,
xref=xref,
overlay=True,
)
doc.save(
"output/watermarked.pdf"
)في هذا السيناريو يتم تخزين الصورة في PDF ثم إعادة استخدام المورد بدل التعامل معها كما لو كانت صورة جديدة في كل صفحة.
هذه من التفاصيل التي غالبًا لا تظهر في أول نسخة من السكربت، لكنها تصبح مهمة عندما يبدأ المشروع في معالجة ملفات كبيرة.
استخدام overlay=False لوضع العلامة المائية في الخلفية
يوفر insert_image() الخيار:
overlay=Trueوعندما يكون True توضع الصورة في المقدمة، أما False فتظهر خلف محتوى الصفحة بحسب ترتيب الرسم. توثيق PyMuPDF يشرح أن overlay يحدد ما إذا كان العنصر يظهر في foreground أو background.
مثلًا:
page.insert_image(
watermark_rect,
filename="watermark.png",
overlay=False,
)هذا مهم جدًا إذا كان الـ watermark مجرد خلفية خفيفة.
ومع ذلك، يجب اختبار النتيجة على أنواع PDF المختلفة. بعض الصفحات قد تحتوي على خلفيات بيضاء كبيرة أو عناصر مرسومة بترتيب يجعل العلامة المائية غير ظاهرة بالشكل المطلوب. ليست كل ملفات PDF متشابهة من ناحية البنية الداخلية.
إضافة Text Watermark مباشرة إلى PDF
لا يشترط أن تنشئ PNG. PyMuPDF يستطيع أيضًا إدراج نص مباشرة في صفحة PDF، مع دعم الشفافية.
مثلًا:
import pymupdf
doc = pymupdf.open(
"input/document.pdf"
)
for page in doc:
rect = page.rect
page.insert_text(
pymupdf.Point(
rect.width / 4,
rect.height / 2,
),
"CONFIDENTIAL",
fontsize=50,
color=(0.5, 0.5, 0.5),
fill_opacity=0.20,
rotate=45,
overlay=True,
)
doc.save(
"output/confidential.pdf"
)توثيق PyMuPDF الحالي يوضح أن Page.insert_text() يدعم fill_opacity وstroke_opacity، كما أن إدخال النص يوفر خيار rotate بزوايا من مضاعفات 90 درجة، بينما توجد آليات أخرى مثل morph للتحويلات الأكثر تعقيدًا.
لكن انتبه إلى أن rotate=45 في المثال السابق لن يكون مقبولًا كدوران عادي في insert_text() وفق القيود الحالية الخاصة بالدوران في هذه الواجهة؛ ولذلك إذا أردت زاوية قطرية مثل 45 درجة، فهناك خياران أفضل: إنشاء صفحة أو صورة watermark مدورة وإدراجها، أو استخدام تقنيات التحويل مثل morph وفق احتياج المشروع. توثيق PyMuPDF يذكر أن الدوران في insert_text() هو بزوايا 0 و90 و180 و270، بينما morph يسمح بتحويلات أكثر مرونة.
لذلك في التطبيقات الإنتاجية سنفضل غالبًا بناء watermark كصورة عندما نحتاج إلى دوران حر.
Watermark نصي في وسط PDF بطريقة صحيحة
يمكن وضع النص بالاعتماد على insert_textbox():
import pymupdf
doc = pymupdf.open(
"input/document.pdf"
)
for page in doc:
rect = page.rect
watermark_rect = pymupdf.Rect(
rect.width * 0.20,
rect.height * 0.40,
rect.width * 0.80,
rect.height * 0.60,
)
page.insert_textbox(
watermark_rect,
"CONFIDENTIAL",
fontname="helv",
fontsize=48,
align=pymupdf.TEXT_ALIGN_CENTER,
color=(0.6, 0.6, 0.6),
fill_opacity=0.18,
overlay=True,
)
doc.save(
"output/confidential.pdf"
)توثيق PyMuPDF يصف insert_textbox() بأنه يضع النص داخل مستطيل محدد، ويقسّم النص إلى أسطر حسب المساحة المتاحة، مع إمكانيات للمحاذاة والحجم والشفافية.
هذا مناسب جدًا للعلامة المائية النصية البسيطة.
ماذا عن النص العربي داخل PDF؟
هنا تبدأ نقطة أكثر حساسية.
النص العربي يحتاج إلى الخط المناسب والتعامل الصحيح مع الأحرف واتجاه الكتابة وتشكيلها. ليس من الحكمة افتراض أن كل خط built-in سيعرض العربية بالشكل الصحيح.
يمكن تمرير ملف خط إلى PyMuPDF:
font_path = "fonts/NotoNaskhArabic-Regular.ttf"ثم:
page.insert_textbox(
watermark_rect,
"وثيقة سرية",
fontfile=font_path,
fontsize=40,
align=pymupdf.TEXT_ALIGN_CENTER,
color=(0.5, 0.5, 0.5),
fill_opacity=0.20,
)توثيق PyMuPDF يوضح أن insert_text() وinsert_textbox() يدعمان fontfile، كما أن المكتبة تتعامل مع الخطوط المخصصة في الصفحة.
مع ذلك، عندما يكون النص العربي معقدًا جدًا أو يحتوي على تشكيل ونص RTL متداخل مع أرقام ونصوص لاتينية، من الضروري اختبار النتيجة البصرية على المستند الحقيقي. وفي بعض المشاريع، قد تكون الصورة المولدة بواسطة Pillow أكثر وضوحًا وأسهل من ناحية التحكم البصري، خصوصًا عندما يكون الهدف هو watermark وليس نصًا يجب أن يبقى قابلًا للتحديد داخل PDF.
طريقة قوية: إنشاء watermark كصورة ثم إدراجها في PDF
هذه الطريقة مفيدة جدًا عندما نريد نتيجة ثابتة.
المسار يصبح:
Text / Logo
↓
Pillow
↓
Transparent PNG
↓
PyMuPDF
↓
PDFلنكتب مثالًا متكاملًا:
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
import pymupdf
def create_watermark(
path,
text,
font_path,
width=1400,
height=700,
):
image = Image.new(
"RGBA",
(width, height),
(0, 0, 0, 0),
)
draw = ImageDraw.Draw(image)
font = ImageFont.truetype(
font_path,
110,
)
bbox = draw.textbbox(
(0, 0),
text,
font=font,
)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = (
width - text_width
) // 2
y = (
height - text_height
) // 2
draw.text(
(x, y),
text,
font=font,
fill=(80, 80, 80, 60),
)
image = image.rotate(
30,
expand=True,
resample=Image.Resampling.BICUBIC,
)
image.save(
path,
"PNG",
)
def watermark_pdf(
input_pdf,
output_pdf,
watermark_image,
):
doc = pymupdf.open(input_pdf)
xref = 0
for page in doc:
rect = page.rect
target = pymupdf.Rect(
rect.width * 0.15,
rect.height * 0.30,
rect.width * 0.85,
rect.height * 0.70,
)
if xref == 0:
xref = page.insert_image(
target,
filename=watermark_image,
keep_proportion=True,
overlay=True,
)
else:
page.insert_image(
target,
xref=xref,
keep_proportion=True,
overlay=True,
)
doc.save(output_pdf)
doc.close()
font_path = "fonts/DejaVuSans.ttf"
watermark_path = "watermark/generated.png"
Path("watermark").mkdir(
exist_ok=True
)
create_watermark(
watermark_path,
"CONFIDENTIAL",
font_path,
)
watermark_pdf(
"input/document.pdf",
"output/document_watermarked.pdf",
watermark_path,
)الجميل في هذا التصميم أنه يفصل بين مرحلتين مستقلتين: توليد العنصر البصري وتطبيقه. ولذلك تستطيع استخدام نفس ملف الـ watermark مع عشرات ملفات PDF.
إضافة شعار داخل PDF بدل نص
لنفرض أن لدينا:
company-logo.pngيمكن إدراجه في الركن العلوي:
import pymupdf
doc = pymupdf.open(
"input/document.pdf"
)
for page in doc:
rect = page.rect
logo_width = 100
logo_rect = pymupdf.Rect(
30,
30,
30 + logo_width,
100,
)
page.insert_image(
logo_rect,
filename="watermark/company-logo.png",
keep_proportion=True,
overlay=True,
)
doc.save(
"output/logo_document.pdf"
)يمكن جعل الموضع ديناميكيًا:
margin = 24
logo_width = 100
x0 = rect.width - logo_width - margin
y0 = margin
x1 = rect.width - margin
y1 = y0 + 70
logo_rect = pymupdf.Rect(
x0,
y0,
x1,
y1,
)وهكذا يظهر الشعار في الركن العلوي الأيمن.
وضع Watermark في أسفل الصفحة
من الاستخدامات الشائعة وضع شعار أو عنوان الموقع في التذييل:
margin = 24
width = 180
height = 60
x0 = (
page.rect.width
- width
- margin
)
y0 = (
page.rect.height
- height
- margin
)
rect = pymupdf.Rect(
x0,
y0,
x0 + width,
y0 + height,
)
page.insert_image(
rect,
filename="watermark/logo.png",
keep_proportion=True,
)هذا النوع من العلامات المائية لا يغطي المحتوى، ولذلك يكون مناسبًا للفواتير والعروض والتقارير.
إضافة Watermark إلى صفحات مختارة فقط
ليس من الضروري أن تضيف العلامة المائية إلى كل الصفحات.
مثال:
for index, page in enumerate(doc):
if index in {0, 2, 4}:
page.insert_image(
page.rect,
filename="watermark.png",
overlay=True,
)أو:
for index, page in enumerate(doc):
if index >= 1:
page.insert_image(
page.rect,
filename="watermark.png",
)بهذا يمكنك تجاهل صفحة الغلاف مثلًا، ووضع watermark في الصفحات الداخلية فقط.
Watermark حسب رقم الصفحة
أحيانًا نريد أن تختلف العلامة المائية بحسب الصفحة:
Internal Copy - Page 1
Internal Copy - Page 2
Internal Copy - Page 3يمكن استخدام Pillow لتوليد watermark مخصص لكل صفحة، لكن هذا يعني إنشاء صور متعددة. إذا كنت تحتاج إلى نص بسيط فقط، قد يكون إدراج النص مباشرة داخل الصفحة أفضل.
import pymupdf
doc = pymupdf.open(
"input/document.pdf"
)
for index, page in enumerate(doc):
page.insert_text(
pymupdf.Point(40, 40),
f"Internal Copy - Page {index + 1}",
fontsize=10,
color=(0.5, 0.5, 0.5),
fill_opacity=0.35,
overlay=True,
)
doc.save(
"output/paged.pdf"
)هنا نستخدم insert_text() لأننا لا نحتاج إلى تصميم بصري معقد.
إضافة اسم العميل إلى PDF
هذا سيناريو عملي جدًا في أنظمة SaaS.
قد تكون لديك وثيقة:
contract.pdfوتريد لكل مستخدم نسخة تحتوي على:
Licensed to: Ahmed
Customer ID: 18392يمكن بناء watermark حسب البيانات:
def watermark_customer_pdf(
input_pdf,
output_pdf,
customer_name,
customer_id,
):
doc = pymupdf.open(input_pdf)
text = (
f"Licensed to: {customer_name}\n"
f"Customer ID: {customer_id}"
)
for page in doc:
rect = page.rect
watermark_rect = pymupdf.Rect(
40,
rect.height - 90,
rect.width - 40,
rect.height - 30,
)
page.insert_textbox(
watermark_rect,
text,
fontsize=10,
color=(0.4, 0.4, 0.4),
fill_opacity=0.50,
align=pymupdf.TEXT_ALIGN_RIGHT,
)
doc.save(output_pdf)
doc.close()هذا النوع من العلامة المائية ليس مجرد Branding؛ إنه أيضًا وسيلة لربط النسخة بمستخدم معين.
استخدام Watermark كحالة للمستند
يمكن أن نستخدم العلامة المائية مثل:
DRAFTأو:
APPROVEDأو:
REVIEW COPYأو:
SAMPLEفي هذه الحالة من الأفضل أن تكون العلامة واضحة لكنها لا تمنع قراءة المحتوى.
مثال:
for page in doc:
rect = page.rect
page.insert_textbox(
pymupdf.Rect(
0,
rect.height / 2 - 50,
rect.width,
rect.height / 2 + 50,
),
"DRAFT",
fontsize=72,
align=pymupdf.TEXT_ALIGN_CENTER,
color=(0.7, 0.7, 0.7),
fill_opacity=0.15,
)هذا النمط شائع في الأنظمة التي تنتج مستندات قابلة للمراجعة قبل الإصدار النهائي.
معالجة PDF كبير جدًا
عند العمل مع PDF كبير، لا يكفي أن يكون الكود صحيحًا. يجب التفكير أيضًا في الذاكرة والزمن وحجم الملف.
افترض أن لديك مستندًا مكوّنًا من مئات الصفحات، وكل صفحة تحتوي على صور عالية الجودة. إضافة watermark إلى كل صفحة قد تنتج ملفًا كبيرًا إذا لم تتم إعادة استخدام الموارد بشكل جيد.
PyMuPDF يوفر آليات لحفظ الملفات مع garbage collection وdeflation، وتوضح وثائقه أن garbage=4 وdeflate=True أو ez_save() قد تكون مفيدة في بعض سيناريوهات تقليل حجم الملف.
يمكن مثلًا:
doc.save(
"output/result.pdf",
garbage=4,
deflate=True,
)لكن يجب ألا تستخدم إعدادات الحفظ الثقيلة دون اختبار. عمليات التنظيف والضغط قد تزيد زمن التنفيذ، وقد لا تقلل الحجم في كل ملف بنفس المقدار.
لماذا حجم Watermark مهم؟
من الأخطاء الشائعة إنشاء PNG ضخم جدًا ثم إدراجه في PDF بحجم صغير. تخيل أنك أنشأت:
watermark.png
4000 × 2000ثم عرضته في الصفحة بعرض:
200 pointsأنت هنا تضيف صورة ضخمة لا تحتاجها فعليًا.
توثيق PyMuPDF ينصح بالنظر في تقليل حجم الصورة قبل إدراجها، لأن هذا قد يحقق توفيرًا كبيرًا في حجم الملف.
لذلك إذا كان watermark سيظهر فعليًا بعرض 300 أو 400 نقطة فقط، لا تحتاج إلى إنشاء صورة 8000 بكسل إلا إذا كان هناك سبب واضح.
الفرق بين العلامة المائية كصورة والعلامة المائية كنص داخل PDF
هناك اختيار معماري مهم جدًا.
عندما تستخدم:
page.insert_textbox(...)يكون العنصر نصًا داخل PDF.
وعندما تستخدم:
page.insert_image(...)فهو صورة.
ميزة النص
النص خفيف غالبًا.
يمكن تغيير المحتوى بسهولة.
قد يبقى قابلًا للتحديد والاستخراج.
قد يكون أكثر ملاءمة لبيانات بسيطة مثل اسم العميل.
ميزة الصورة
تحكم بصري أكبر.
يمكن أن تحتوي على شعار وأيقونات وتصميمات.
يمكن تدويرها بأي زاوية قبل الإدراج.
يمكن أن تحتوي على تعقيدات بصرية لا تريد إعادة بنائها داخل PDF.
لهذا السبب ليس هناك "أفضل" اختيار بشكل مطلق. الاختيار يعتمد على طبيعة الـ watermark.
تصميم Watermark بشكل احترافي
المستخدم غالبًا لا يريد أن يرى العلامة المائية فقط؛ يريد أن يحصل على مستند أو صورة تبدو احترافية.
هناك بعض المبادئ العملية المفيدة.
لا تجعل النص كبيرًا بشكل مبالغ فيه إلا عندما تكون وظيفته هي إعلان الحالة مثل CONFIDENTIAL.
استخدم شفافية منخفضة عندما يكون الـ watermark علامة تجارية، وشفافية أعلى عندما تريد ضمان ظهوره بوضوح.
ضع watermark في مساحة لا تحتوي عادةً على تفاصيل مهمة.
بالنسبة للصور، جرّب الوضع السفلي الأيسر أو السفلي الأيمن.
بالنسبة للوثائق، ضع الشعار في header أو footer إذا كان الهدف هو Branding، واستخدم watermark قطريًا عندما يكون الهدف هو توضيح حالة المستند.
وإذا كانت الصور تختلف كثيرًا في درجات السطوع، فلا تستخدم دائمًا لونًا واحدًا. في الصور الداكنة قد يكون الأبيض مناسبًا، بينما في الصور الفاتحة قد يبدو الأبيض غير واضح. يمكن في مشروع أكثر تقدمًا تحليل متوسط الإضاءة واختيار لون مناسب.
اختيار لون Watermark تلقائيًا للصورة
يمكن حساب إضاءة تقريبية للصورة:
from PIL import Image, ImageStat
def choose_watermark_color(image):
small = image.convert("L")
small.thumbnail((100, 100))
mean = ImageStat.Stat(
small
).mean[0]
if mean < 128:
return (255, 255, 255)
return (0, 0, 0)ثم:
color = choose_watermark_color(image)
draw.text(
position,
text,
font=font,
fill=(*color, 100),
)هذا ليس نظامًا مثاليًا لفهم كل صورة، لكنه حل بسيط وعملي لتحسين وضوح العلامة المائية.
إنشاء إطار صغير حول Watermark
أحيانًا يكون النص الأبيض فوق الخلفية صعب القراءة. يمكن إنشاء stroke:
draw.text(
(x, y),
"example.com",
font=font,
fill=(255, 255, 255, 130),
stroke_width=2,
stroke_fill=(0, 0, 0, 100),
)هذا يعطي النص حافة خفيفة تساعد على ظهوره فوق التفاصيل.
ويصبح مناسبًا خصوصًا للصور التي تحتوي على خلفيات متعددة الألوان.
إنشاء شريط شفاف خلف العلامة المائية
هناك تصميم آخر:
┌────────────────────────────┐
│ │
│ │
│ IMAGE │
│ │
│ ┌──────────────────────┐ │
│ │ example.com │ │
│ └──────────────────────┘ │
└────────────────────────────┘يمكن إنشاء مستطيل شفاف أولًا:
draw.rounded_rectangle(
(x0, y0, x1, y1),
radius=15,
fill=(0, 0, 0, 90),
)ثم النص:
draw.text(
(x0 + 20, y0 + 12),
text,
font=font,
fill=(255, 255, 255, 180),
)هذه الطريقة ممتازة للصور المعروضة في المواقع أو الشبكات الاجتماعية.
إنشاء Watermark كطبقة مستقلة
من أفضل القرارات في تصميم الكود ألا تقوم بتعديل الصورة الأصلية مباشرة.
بدل:
image = ...
draw = ImageDraw.Draw(image)استخدم:
overlay = Image.new(
"RGBA",
image.size,
(0, 0, 0, 0)
)ثم:
draw = ImageDraw.Draw(overlay)وبعد ذلك:
result = Image.alpha_composite(
image,
overlay
)هذا يجعل من السهل تعديل العلامة المائية أو إعادة إنشائها أو اختبارها بدون التأثير على الصورة الأساسية أثناء خطوات المعالجة.
بناء Class لإدارة العلامة المائية
إذا كان المشروع يكبر، يمكنك الانتقال من الدوال إلى Class:
from PIL import Image, ImageDraw, ImageFont
class ImageWatermarker:
def __init__(
self,
font_path,
font_size=48,
opacity=100,
):
self.font = ImageFont.truetype(
font_path,
font_size,
)
self.opacity = opacity
def apply(
self,
image_path,
output_path,
text,
position="bottom-right",
margin=40,
):
image = Image.open(
image_path
).convert("RGBA")
overlay = Image.new(
"RGBA",
image.size,
(0, 0, 0, 0),
)
draw = ImageDraw.Draw(
overlay
)
bbox = draw.textbbox(
(0, 0),
text,
font=self.font,
)
width = bbox[2] - bbox[0]
height = bbox[3] - bbox[1]
x, y = calculate_position(
image.size,
(width, height),
position,
margin,
)
draw.text(
(x, y),
text,
font=self.font,
fill=(
255,
255,
255,
self.opacity,
),
)
result = Image.alpha_composite(
image,
overlay,
)
result.convert(
"RGB"
).save(
output_path,
quality=90,
)الاستخدام:
watermarker = ImageWatermarker(
font_path="arial.ttf",
font_size=48,
opacity=100,
)
watermarker.apply(
image_path="input/photo.jpg",
output_path="output/photo.jpg",
text="example.com",
)هذا التصميم أفضل عندما توجد إعدادات كثيرة مشتركة بين الملفات.
بناء خدمة موحدة للصور وPDF
يمكننا بناء واجهة أعلى مستوى:
class WatermarkService:
def watermark_image(
self,
source,
destination,
watermark,
):
...
def watermark_pdf(
self,
source,
destination,
watermark,
):
...وفي نظام API يمكن أن تكون:
POST /watermark/image
POST /watermark/pdfثم يرسل المستخدم:
{
"text": "example.com",
"position": "bottom-right",
"opacity": 100
}ويقوم الخادم بتحميل الملف وتشغيل المعالجة وإرجاع الملف الناتج.
مثال FastAPI لإضافة Watermark إلى صورة
يمكننا دمج Pillow بسهولة مع FastAPI.
pip install fastapi uvicorn python-multipartثم:
from pathlib import Path
from uuid import uuid4
from fastapi import FastAPI, UploadFile, File
from fastapi.responses import FileResponse
from PIL import Image, ImageDraw, ImageFont
app = FastAPI()
UPLOAD_DIR = Path("uploads")
OUTPUT_DIR = Path("outputs")
UPLOAD_DIR.mkdir(exist_ok=True)
OUTPUT_DIR.mkdir(exist_ok=True)
FONT_PATH = "arial.ttf"
@app.post("/watermark/image")
async def watermark_image(
file: UploadFile = File(...)
):
extension = Path(
file.filename
).suffix.lower()
source = (
UPLOAD_DIR
/ f"{uuid4()}{extension}"
)
output = (
OUTPUT_DIR
/ f"{uuid4()}.jpg"
)
source.write_bytes(
await file.read()
)
image = Image.open(
source
).convert("RGBA")
overlay = Image.new(
"RGBA",
image.size,
(0, 0, 0, 0),
)
draw = ImageDraw.Draw(
overlay
)
font = ImageFont.truetype(
FONT_PATH,
48,
)
draw.text(
(30, 30),
"example.com",
font=font,
fill=(255, 255, 255, 100),
)
result = Image.alpha_composite(
image,
overlay,
)
result.convert(
"RGB"
).save(
output,
quality=90,
)
return FileResponse(
output,
media_type="image/jpeg",
filename="watermarked.jpg",
)هذا مجرد مثال تعليمي. في الإنتاج يجب إضافة validation لنوع الملف وحجمه، ومعالجة الاستثناءات، وتنظيف الملفات المؤقتة، وفرض حدود للموارد.
لماذا Validation مهم في Web API؟
عندما يستقبل خادمك ملفات من الإنترنت، لا يجب أن تفترض أن كل ما يصل هو صورة أو PDF صحيح.
مثلًا:
content_type = file.content_typeيمكن أن يساعد كإشارة أولية، لكنه ليس كافيًا وحده للحماية.
يجب أن تتحقق من:
حجم الملف.
امتداده.
نوع المحتوى.
قدرة المكتبة فعلًا على فتح الملف.
الحد الأقصى لأبعاد الصورة.
الحد الأقصى لعدد صفحات PDF.
هذه النقطة مهمة لأن العمليات على الصور يمكن أن تستهلك ذاكرة ضخمة عندما تكون الأبعاد كبيرة جدًا.
الحماية من الصور الضخمة
في بعض البيئات، صورة بأبعاد ضخمة قد تسبب استهلاكًا كبيرًا للذاكرة أثناء التحويل أو إنشاء طبقة RGBA بنفس الحجم.
يمكن وضع حد منطقي:
MAX_PIXELS = 40_000_000
if image.width * image.height > MAX_PIXELS:
raise ValueError(
"Image dimensions are too large"
)لا تجعل الحد رقمًا عشوائيًا في تطبيق حقيقي؛ اختره وفق موارد الخادم ونوع الصور الذي يستقبله النظام.
التعامل مع PDF غير الصالح
قد يفشل:
doc = pymupdf.open(path)لأن الملف ليس PDF صالحًا أو لأنه تالف.
استخدم:
try:
doc = pymupdf.open(path)
except Exception as exc:
raise ValueError(
"Invalid PDF file"
) from excثم لا تنسَ إغلاقه:
doc.close()أو يمكنك استخدام سياق مناسب إذا كان إصدار المكتبة وطريقة عملك يسمحان بذلك.
استخدام with عند فتح ملفات الصور
مع Pillow من الأفضل أن تكون واضحًا مع الموارد:
from PIL import Image
with Image.open("input/photo.jpg") as image:
image = image.convert("RGBA")
...هذا يساعد على إدارة الملف بشكل أفضل، خصوصًا في العمليات الدفعية.
لكن تذكر أن:
image = image.convert("RGBA")ينشئ صورة جديدة، ولذلك إذا كنت ستستخدمها خارج سياق with، يجب أن تتأكد أن النسخة الموجودة في الذاكرة مستقلة عن الملف المفتوح.
إنشاء CLI Tool لإضافة Watermark
يمكن تحويل المشروع إلى أداة سطر أوامر.
مثلًا:
python main.py image \
--input input/photo.jpg \
--output output/photo.jpg \
--text "example.com" \
--opacity 100ولـ PDF:
python main.py pdf \
--input input/document.pdf \
--output output/document.pdf \
--watermark watermark/logo.pngيمكن استخدام argparse.
import argparse
parser = argparse.ArgumentParser(
description="Watermark images and PDFs"
)
subparsers = parser.add_subparsers(
dest="command",
required=True,
)
image_parser = subparsers.add_parser(
"image"
)
image_parser.add_argument(
"--input",
required=True,
)
image_parser.add_argument(
"--output",
required=True,
)
image_parser.add_argument(
"--text",
required=True,
)
image_parser.add_argument(
"--opacity",
type=int,
default=100,
)
args = parser.parse_args()ثم:
if args.command == "image":
add_text_watermark(
args.input,
args.output,
args.text,
"arial.ttf",
opacity=args.opacity,
)بهذا يصبح لديك برنامج فعلي يمكن تشغيله من Terminal بدل ملف Python يتم تعديله يدويًا كل مرة.
إضافة Logging
لا تعتمد على:
print("done")في أداة إنتاجية كبيرة.
استخدم:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
logger = logging.getLogger(__name__)ثم:
logger.info(
"Processing file: %s",
image_path
)وعند الخطأ:
logger.exception(
"Watermark failed for %s",
image_path
)هذا يجعل تشخيص المشاكل أسهل بكثير عندما تتم معالجة آلاف الملفات.
اختبار Watermark للصور
من المهم ألا تعتمد على النظر إلى صورة واحدة فقط.
اختبر:
صورة صغيرة
صورة كبيرة
صورة Portrait
صورة Landscape
صورة PNG شفافة
صورة JPG
صورة فاتحة جدًا
صورة داكنة جدًا
صورة بأبعاد غير اعتياديةوبالنسبة إلى العلامة المائية:
نص قصير
نص طويل
اسم عربي
اسم إنجليزي
اسم يحتوي على أرقام
Logo شفاف
Logo بدون شفافيةكل حالة يمكن أن تكشف مشكلة مختلفة.
اختبار Watermark للـ PDF
وبالنسبة إلى PDF:
صفحة واحدة
5 صفحات
100 صفحة
صفحات Portrait
صفحات Landscape
PDF يحتوي على صور كثيرة
PDF يحتوي على نص كثير
PDF يحتوي على خطوط مخصصة
PDF يحتوي على روابط
PDF محمي
PDF كبير الحجمراقب ثلاثة أشياء على الأقل:
هل watermark يظهر؟
هل النص الأصلي ما زال مقروءًا؟
هل حجم الملف النهائي مقبول؟
ثم اختبر أيضًا فتح الملف في أكثر من قارئ PDF، لأن سلوك العرض قد يختلف حسب العارض.
التحقق من وجود Watermark بعد المعالجة
في بيئة الاختبارات، يمكن التحقق من وجود الصورة أو النص بطريقة محدودة.
مثلًا لو استخدمت نصًا مباشرًا:
doc = pymupdf.open(
"output/confidential.pdf"
)
for page in doc:
text = page.get_text()
assert "CONFIDENTIAL" in text
doc.close()لكن هذه الطريقة لا تنطبق عندما يكون watermark صورة.
بالنسبة للصورة، يمكننا اختبار حجم الملف أو فحص موارد الصفحة، لكن الاختبار الأفضل غالبًا يكون بصريًا أو باستخدام rendering ومقارنة الصورة الناتجة بسلوك متوقع.
Rendering لصفحات PDF للتحقق البصري
يمكن لـ PyMuPDF تحويل الصفحة إلى صورة عبر get_pixmap(). توثيق PyMuPDF يذكر أن get_pixmap() يمكن استخدامه لتصيير الصفحة، مع إمكانية تحديد DPI مثل 150 أو 300.
مثلًا:
import pymupdf
doc = pymupdf.open(
"output/document_watermarked.pdf"
)
page = doc[0]
pix = page.get_pixmap(
dpi=150
)
pix.save(
"output/preview.png"
)
doc.close()يمكن استخدام هذه التقنية في اختبارات CI المتقدمة لإنشاء preview للصفحة والتحقق منها.
استخدام Watermark على الصور قبل تحويلها إلى PDF
هناك حالة مثيرة للاهتمام: لديك مجموعة صور وتريد إنشاء PDF ثم إضافة watermark.
يمكن تنفيذ:
Image
↓
Pillow
↓
Watermarked Image
↓
PDFلكن في حالات كثيرة من الأفضل تجنب تحويل المحتوى إلى صور إذا كان لديك PDF أصلي يحتوي على نصوص وروابط وطبقات مفيدة. إذا كانت لديك صورة أصلًا، فمعالجة الصورة مباشرة ثم تحويلها إلى PDF قد يكون منطقيًا.
PyMuPDF يتيح أيضًا العمل مع الصور كوثائق وتحويلها إلى PDF، لكن توثيقه يذكر ملاحظة مهمة: عند التحويل بهذه الطريقة قد يتم تجاهل شفافية الصور، ولذلك إذا كنت تحتاج إلى الشفافية المرتبطة بالـ watermark فإن Page.insert_image() يكون خيارًا مناسبًا.
الفرق بين Raster Watermark وVector Watermark
عندما ننشئ العلامة المائية بواسطة Pillow، نحصل عادةً على صورة raster. إذا كانت صورة كبيرة، قد تكون الجودة ممتازة ضمن الحجم المطلوب، لكنها تظل مبنية على pixels.
أما إذا استخدمنا صفحة PDF كعلامة مائية عبر show_pdf_page()، يمكن أن يكون المحتوى متجهيًا. توثيق PyMuPDF يشرح أن show_pdf_page() يعرض صفحة PDF داخل مستطيل آخر، ويشير إلى أنه مناسب أيضًا لعلامات مائية مبنية على PDF ويمكن أن يحافظ على طبيعة المحتوى المتجهي.
هذا يصبح مفيدًا عندما تكون العلامة المائية عبارة عن شعار متجهي معقد أو تصميم تريد أن يحتفظ بدقته عند التكبير.
استخدام PDF كـ Watermark
يمكن إنشاء ملف:
watermark.pdfثم:
import pymupdf
watermark_doc = pymupdf.open(
"watermark/watermark.pdf"
)
doc = pymupdf.open(
"input/document.pdf"
)
for page in doc:
page.show_pdf_page(
page.rect,
watermark_doc,
0,
overlay=True,
)
doc.save(
"output/result.pdf"
)
watermark_doc.close()
doc.close()توثيق PyMuPDF يوضح هذا النمط صراحةً كطريقة لعمل stamp أو watermark عن طريق عرض صفحة PDF فوق صفحة أخرى.
ميزة هذا الأسلوب أن watermark نفسه يمكن أن يكون PDF يحتوي على تصميم متجهي أو أكثر تعقيدًا من مجرد صورة.
متى نستخدم show_pdf_page() بدل insert_image()؟
استخدم insert_image() عندما يكون watermark عبارة عن صورة PNG أو JPEG أو صورة في الذاكرة.
استخدم show_pdf_page() عندما يكون watermark نفسه صفحة PDF، أو عندما تحتاج إلى محتوى متجهي أو دورانًا أكثر مرونة. توثيق PyMuPDF يوضح أن show_pdf_page() يدعم تدويرًا أكثر عمومية، بينما insert_image() يقبل دورانًا بمضاعفات 90 درجة.
هذا الفرق قد يبدو تقنيًا، لكنه مهم في المشاريع التي تتطلب تصميم watermark متقدمًا.
Watermark متعدد الطبقات
يمكن إنشاء watermark يتكون من:
Logo
+
Company Name
+
Website
+
Customer ID
+
Dateباستخدام Pillow:
canvas = Image.new(
"RGBA",
(1200, 600),
(0, 0, 0, 0)
)ثم تضع العناصر واحدًا تلو الآخر:
draw.text(...)
canvas.alpha_composite(...)
draw.text(...)
draw.text(...)ثم:
canvas.rotate(...)وفي النهاية:
canvas.save(
"watermark.png"
)ثم يستخدم نفس watermark في PDF.
هذا التصميم ممتاز عندما تريد تحقيق هوية بصرية موحدة بين الصور والمستندات.
إنشاء Watermark ديناميكي باستخدام التاريخ
يمكن إضافة التاريخ:
from datetime import datetime
today = datetime.now().strftime(
"%Y-%m-%d"
)
text = (
f"Confidential | "
f"{today}"
)ثم يتم رسمه داخل watermark.
في نظام SaaS يمكن أن يكون:
text = (
f"Licensed to {customer_name} | "
f"{customer_id} | "
f"{today}"
)وهنا يصبح كل ملف مميزًا.
إضافة رمز UUID للنسخة
إذا أردت تتبع النسخة:
from uuid import uuid4
watermark_id = str(uuid4())
text = (
f"Document ID: {watermark_id}"
)يمكن وضع هذا المعرف في أسفل الصفحة أو بشكل شفاف.
الفائدة هي أنك تستطيع معرفة النسخة التي خرجت من النظام، لكن يجب الانتباه إلى أن watermark المرئي ليس بديلًا عن أنظمة تتبع الملفات والصلاحيات. هو مجرد عنصر إضافي يساعد في التعريف والتتبع.
التكامل مع Django
في تطبيق Django قد يكون السيناريو:
User uploads image
↓
Django receives file
↓
Save original
↓
Create watermark
↓
Save processed version
↓
Store path in databaseداخل service:
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
def process_uploaded_image(
source_path,
destination_path,
watermark_text,
):
image = Image.open(
source_path
).convert("RGBA")
overlay = Image.new(
"RGBA",
image.size,
(0, 0, 0, 0)
)
draw = ImageDraw.Draw(
overlay
)
font = ImageFont.truetype(
"fonts/arial.ttf",
48,
)
draw.text(
(30, 30),
watermark_text,
font=font,
fill=(255, 255, 255, 100),
)
output = Image.alpha_composite(
image,
overlay
)
output.convert(
"RGB"
).save(
destination_path,
quality=90
)ويستطيع model أن يحتفظ بنسخة أصلية ونسخة معالجة.
وهنا توجد نقطة عملية مهمة جدًا: لا تكتب فوق الملف الأصلي إلا إذا كنت متأكدًا أن هذا هو السلوك المطلوب. في كثير من الأنظمة من الأفضل الاحتفاظ بالنسخة الأصلية منفصلة عن النسخة المعدلة.
التكامل مع Celery
إذا كان المستخدم يرفع صورة بحجم كبير، لا تريد أن يقوم HTTP request بعملية تستغرق ثواني طويلة أو أكثر.
بدلًا من ذلك:
Upload
↓
Database
↓
Celery Task
↓
Watermark
↓
Saveمثلًا:
from celery import shared_task
@shared_task
def watermark_image_task(
source,
destination,
):
add_text_watermark(
source,
destination,
"example.com",
"fonts/arial.ttf",
opacity=100,
)ثم:
watermark_image_task.delay(
source,
destination,
)هذا النمط مناسب للملفات الكبيرة أو للمعالجة الدفعية.
التعامل مع الملفات المؤقتة
إذا كان التطبيق Web API ويستقبل مئات الملفات، ستحتاج إلى سياسة واضحة للملفات المؤقتة.
مثلًا:
/tmp/watermark/ثم:
source.unlink(
missing_ok=True
)بعد نجاح المعالجة.
لكن لا تحذف الأصل قبل التأكد من نجاح عملية الحفظ.
ترتيب أفضل:
Upload
↓
Validate
↓
Process
↓
Save output
↓
Verify output exists
↓
Delete temporary sourceهذا يحميك من سيناريوهات يكون فيها الملف الناتج ناقصًا أو فشلت العملية أثناء منتصفها.
التعامل مع أسماء الملفات بأمان
لا تستخدم اسم الملف الذي يرسله المستخدم مباشرة في مسار النظام:
Path("/uploads") / file.filenameبدون تنظيف.
من الأفضل إنشاء أسماء عشوائية:
from uuid import uuid4
safe_name = (
f"{uuid4()}.jpg"
)ثم حفظ الملف بهذه التسمية.
هذه ليست مجرد مسألة Watermark؛ إنها قاعدة مهمة عند بناء أي خدمة رفع ملفات.
دعم WebP
Pillow يدعم WebP في البيئات المناسبة، ويمكن فتح:
image = Image.open(
"input/photo.webp"
)ثم معالجة الصورة وحفظ الناتج إلى:
result.save(
"output/photo.webp",
quality=90,
)لكن إذا كنت تريد إخراجًا مضمونًا في كل بيئة، JPG وPNG عادةً أسهل من ناحية التوافق العام، بينما WebP خيار ممتاز للويب عندما تتحكم في البيئة المستقبلة.
الحفاظ على الشفافية في الإخراج
لو كانت الصورة الأصلية PNG وتحتاج إلى إبقاء الشفافية، لا تحولها في النهاية إلى RGB:
بدل:
result.convert("RGB").save(
"result.png"
)استخدم:
result.save(
"result.png"
)أما إذا كنت ستكتب JPEG:
result.convert("RGB").save(
"result.jpg",
quality=90,
)لأن JPEG لا يملك قناة Alpha بالشكل الذي يوفره PNG.
Watermark على PNG شفاف
لنفترض أن لديك صورة منتج بخلفية شفافة وتريد الحفاظ على الشفافية:
image = Image.open(
"product.png"
).convert("RGBA")
overlay = Image.new(
"RGBA",
image.size,
(0, 0, 0, 0)
)
draw = ImageDraw.Draw(
overlay
)
draw.text(
(20, 20),
"example.com",
font=font,
fill=(255, 255, 255, 100),
)
result = Image.alpha_composite(
image,
overlay
)
result.save(
"product_watermarked.png"
)هنا لم نقم بتحويل الصورة إلى RGB.
Watermark على صورة CMYK
في بعض البيئات الاحترافية قد تتعامل مع صور CMYK، خصوصًا في الطباعة.
لا تفترض أن كل الملفات يجب أن تعالج بنفس الطريقة:
image = Image.open(path)
print(image.mode)قد تكون:
RGB
RGBA
CMYK
L
PPillow يوفر مجموعة من أوضاع الصور، ومنها RGB وRGBA وCMYK وL وغيرها.
إذا كان التطبيق يحتاج إلى الحفاظ على CMYK لأغراض الطباعة، يجب تصميم pipeline كامل يعرف متى يقوم بالتحويل، بدل تحويل جميع الملفات عشوائيًا إلى RGB.
معالجة EXIF وبيانات الصورة
عند حفظ الصورة، قد لا تنتقل جميع معلومات EXIF بنفس الطريقة أو بنفس الشكل الذي تتوقعه. إذا كان المشروع يعتمد على بيانات مثل:
Camera Model
GPS
Date Takenيجب أن تتعامل معها بشكل منفصل.
وهنا توجد نقطة خصوصية مهمة: صور المستخدمين قد تحتوي على GPS أو معلومات حساسة. إذا كنت تطبق watermark ثم تنشر الصورة، فكر فيما إذا كنت تحتاج فعلًا إلى الاحتفاظ بكل metadata.
إزالة بيانات GPS قبل النشر
في سيناريوهات معينة، قد يكون من الأفضل إنشاء نسخة للنشر بدون معلومات موقع جغرافي.
لا تعتمد على watermark في حماية الخصوصية. العلامة المائية عنصر بصري، بينما بيانات EXIF يمكن أن تحمل معلومات مختلفة تمامًا.
إذا كان المشروع يتعامل مع صور حساسة، صمّم خطوة مستقلة لإدارة metadata.
Watermark ليس تشفيرًا
هذه نقطة تستحق التأكيد.
إضافة:
CONFIDENTIALإلى PDF لا تعني أن الملف أصبح مشفرًا.
وإضافة:
example.comإلى صورة لا تمنع نسخ الصورة.
Watermark هو طبقة مرئية فوق المحتوى.
إذا كنت تحتاج إلى حماية حقيقية للمحتوى، ستحتاج إلى إجراءات أخرى مثل صلاحيات الوصول، روابط مؤقتة، تشفير أثناء النقل والتخزين، التحكم في التنزيل، أو تشفير PDF حسب السيناريو.
العلامة المائية مكملة لهذه الإجراءات وليست بديلًا عنها.
التعامل مع PDF المحمي بكلمة مرور
قد تواجه ملفًا محميًا.
PyMuPDF يتيح التعامل مع خصائص وثيقة PDF وإجراءات المصادقة، لكن يجب أن تضع سياسة واضحة في التطبيق: هل تقبل مستندًا محميًا إذا قدم المستخدم كلمة المرور؟ هل ترفضه؟ هل يجب فك الحماية مؤقتًا ثم إعادة تطبيق حماية مناسبة؟
لا تحاول تجاوز حماية مستند لا تملك صلاحية التعامل معه. في نظام تجاري، يجب أن تكون قواعد معالجة الملفات واضحة ومشروعة.
إنشاء ملف إعدادات Watermark
بدل وضع القيم داخل الكود:
opacity=100
font_size=48
margin=40
position="bottom-right"يمكن إنشاء:
{
"text": "example.com",
"position": "bottom-right",
"opacity": 100,
"font_size": 48,
"margin": 40
}ثم تحميله:
import json
with open(
"watermark.json",
"r",
encoding="utf-8"
) as file:
config = json.load(file)وهذا يجعل تغيير الإعدادات أسهل دون تعديل الكود.
في المشاريع الأكبر يمكن حفظ الإعدادات في قاعدة البيانات.
إضافة أكثر من Watermark
يمكن وضع:
Logo -> top-right
Website -> bottom-left
Customer ID -> centerمثلًا:
add_logo_watermark(...)
add_text_watermark(...)
add_customer_id_watermark(...)لكن إذا كان جميعها يستخدم نفس الطبقة، فمن الأفضل بناء compositing pipeline واحد.
مثال:
overlay = Image.new(
"RGBA",
image.size,
(0, 0, 0, 0)
)
# logo
overlay.alpha_composite(...)
# text
draw = ImageDraw.Draw(overlay)
draw.text(...)
# id
draw.text(...)
result = Image.alpha_composite(
image,
overlay
)ميزة ذلك أنك تقوم بعملية دمج رئيسية واحدة بدل دمج الصورة بعد كل عنصر.
إضافة Pattern Watermark متكرر
بعض الشركات لا تريد watermark واحدًا فقط، بل تكرر الاسم قطريًا:
CONFIDENTIAL CONFIDENTIAL
CONFIDENTIAL CONFIDENTIAL
CONFIDENTIAL CONFIDENTIALيمكن بناء watermark tile ثم تكراره.
فكرة مبسطة:
tile = Image.new(
"RGBA",
(500, 300),
(0, 0, 0, 0)
)
draw = ImageDraw.Draw(tile)
draw.text(
(50, 100),
"CONFIDENTIAL",
font=font,
fill=(100, 100, 100, 35)
)
tile = tile.rotate(
30,
expand=True,
)ثم تكراره على كامل الصورة عبر خطوات x وy.
يمكن استخدام:
for y in range(
-tile.height,
image.height,
tile.height
):
for x in range(
-tile.width,
image.width,
tile.width
):
overlay.alpha_composite(
tile,
(x, y)
)وهذا ينتج pattern موزعًا على مساحة الصورة.
Watermark في PDF باستخدام Pattern
يمكن إنشاء pattern كـ PNG كبير يحتوي على عدة نسخ من النص، ثم إدراجه في منتصف الصفحة أو على مساحة كاملة.
لكن انتبه إلى أن PNG الكبير سيؤثر في حجم PDF. في الحالات التي تحتاج فيها إلى مستندات عالية الجودة على نطاق واسع، قد تكون صفحة PDF متجهية عبر show_pdf_page() أكثر مناسبة.
توثيق PyMuPDF يفرق بوضوح بين insert_image() الذي يعتمد على صورة وshow_pdf_page() الذي يضع صفحة PDF أخرى، ويشير إلى أن الثاني يمكن أن يستخدم محتوى متجهيًا.
تحسين سرعة معالجة الصور
أكبر مشكلة غالبًا ليست في رسم النص، وإنما في التعامل مع الصور الضخمة.
إذا كانت الصورة:
8000 × 6000فإنها تحتوي على عدد ضخم من البكسلات.
وإذا أنشأت:
overlay = Image.new(
"RGBA",
image.size,
...
)فأنت تنشئ طبقة بنفس الحجم.
هذا منطقي، لكنه يستهلك الذاكرة.
لذلك إذا كنت تعرف أن الصور يجب أن تكون مثلًا 2000px كحد أقصى للعرض على الويب، يمكن تصغيرها أولًا:
image.thumbnail(
(2000, 2000),
Image.Resampling.LANCZOS
)ثم إنشاء الـ watermark.
لكن لا تطبق هذا على ملفات تحتاج إلى الدقة الأصلية إلا إذا كان ذلك جزءًا من متطلبات المشروع.
التعامل مع اتجاه Landscape وPortrait
استخدام نسبة من image.width أو page.rect.width يساعد كثيرًا.
مثال:
target_width = int(
image.width * 0.20
)بدل:
target_width = 300النسبة أكثر مرونة.
يمكن أن تقول:
Watermark width = 18% of image widthفتعمل نفس الخوارزمية على صور صغيرة وكبيرة وعمودية وأفقية.
إنشاء Configuration مرن
يمكن تعريف:
WATERMARK_CONFIG = {
"text": "example.com",
"opacity": 100,
"position": "bottom-right",
"scale": 0.18,
"margin": 30,
}ثم:
def process_image(
path,
output,
config
):
...هذا يسمح لك بتغيير كل شيء من مكان واحد.
إنشاء Watermark Service كامل
يمكن الجمع بين كل ما سبق:
from pathlib import Path
from PIL import (
Image,
ImageDraw,
ImageFont,
ImageOps,
)
class WatermarkService:
def __init__(
self,
font_path,
font_size=48,
):
self.font = ImageFont.truetype(
font_path,
font_size,
)
def add_text_to_image(
self,
source,
destination,
text,
position="bottom-right",
opacity=100,
margin=30,
):
image = Image.open(
source
)
image = ImageOps.exif_transpose(
image
)
image = image.convert(
"RGBA"
)
overlay = Image.new(
"RGBA",
image.size,
(0, 0, 0, 0),
)
draw = ImageDraw.Draw(
overlay
)
bbox = draw.textbbox(
(0, 0),
text,
font=self.font,
)
width = (
bbox[2] - bbox[0]
)
height = (
bbox[3] - bbox[1]
)
x, y = calculate_position(
image.size,
(width, height),
position,
margin,
)
draw.text(
(x, y),
text,
font=self.font,
fill=(
255,
255,
255,
opacity,
),
)
result = Image.alpha_composite(
image,
overlay,
)
suffix = (
Path(destination)
.suffix
.lower()
)
if suffix in {
".jpg",
".jpeg",
}:
result = result.convert(
"RGB"
)
result.save(
destination
)هذه البنية قريبة جدًا من النوع الذي يمكنك تحويله لاحقًا إلى package صغيرة داخل مشروعك.
مثال FastAPI للـ PDF
المنطق مشابه:
import tempfile
from pathlib import Path
import pymupdf
from fastapi import (
FastAPI,
File,
UploadFile,
)
from fastapi.responses import FileResponse
app = FastAPI()
@app.post("/watermark/pdf")
async def watermark_pdf(
file: UploadFile = File(...)
):
with tempfile.TemporaryDirectory() as tmp:
source = (
Path(tmp)
/ "input.pdf"
)
output = (
Path(tmp)
/ "output.pdf"
)
source.write_bytes(
await file.read()
)
doc = pymupdf.open(
source
)
xref = 0
for page in doc:
if xref == 0:
xref = page.insert_image(
page.rect,
filename="watermark/logo.png",
overlay=True,
)
else:
page.insert_image(
page.rect,
xref=xref,
overlay=True,
)
doc.save(output)
doc.close()
return FileResponse(
output,
media_type="application/pdf",
filename="watermarked.pdf",
)في خدمة فعلية يجب التعامل مع مشكلة مهمة: الملف الموجود داخل TemporaryDirectory() سيُحذف عند انتهاء البلوك، في حين أن response قد يحتاج الملف بعد تلك اللحظة حسب طريقة الإرسال. يمكن حل ذلك باستخدام StreamingResponse أو قراءة المحتوى إلى الذاكرة إذا كان الحجم مناسبًا أو تخزين الناتج في مكان دائم ثم تنظيفه لاحقًا. هذه التفاصيل الصغيرة مهمة جدًا في APIs الحقيقية.
تخزين نتيجة PDF في الذاكرة
PyMuPDF يمكنه العمل مع البيانات في الذاكرة أيضًا. في بعض السيناريوهات الصغيرة قد يكون من المفيد استخدام:
pdf_bytes = doc.tobytes()ثم إرجاع البيانات بواسطة:
from fastapi.responses import Response
return Response(
content=pdf_bytes,
media_type="application/pdf",
)لكن لا تفعل ذلك مع ملفات ضخمة بشكل غير مدروس، لأنك قد تضع الملف كاملًا في الذاكرة. بالنسبة للملفات الكبيرة، التخزين المؤقت أو streaming قد يكون أفضل.
التوافق مع أنظمة التشغيل
Pillow وPyMuPDF يعملان في بيئات شائعة متعددة، لكن الخطوط تحديدًا يمكن أن تختلف من نظام إلى آخر.
لا تعتمد على:
arial.ttfأن يكون موجودًا على Linux لمجرد أنه موجود على Windows.
الأفضل أن تحفظ الخط المطلوب في مشروعك إذا كانت رخصته تسمح بذلك:
fonts/
NotoSans-Regular.ttfثم:
font_path = (
"fonts/NotoSans-Regular.ttf"
)وهكذا يصبح الناتج متوقعًا في بيئة التطوير والإنتاج.
لا تخلط بين fitz وpymupdf بلا سبب
ستجد كثيرًا من الأمثلة القديمة:
import fitzبينما التوثيق الحديث يستخدم:
import pymupdfوتوضح وثائق PyMuPDF أن الاسم الحديث هو pymupdf مع وجود توافق تاريخي مع fitz، كما تحذر من وجود حزمة PyPI منفصلة تسمى fitz لا علاقة لها بالمكتبة الصحيحة.
لذلك في مشروع جديد:
import pymupdfهو الأسلوب الواضح والمتوافق مع التوثيق الحالي.
أخطاء شائعة عند تطبيق Watermark على الصور
الخطأ الأول: رسم الشفافية مباشرة على RGB
draw.text(
...,
fill=(255, 255, 255, 80)
)ثم توقع أن تصبح النتيجة نصف شفافة. الحل الأكثر موثوقية هو استخدام طبقة RGBA مستقلة ثم compositing.
الخطأ الثاني: وضع watermark بمقاس ثابت
logo.resize((300, 100))قد يبدو جيدًا في صورة ويبدو سيئًا في صورة أخرى.
الخطأ الثالث: تشويه الشعار
تغيير العرض والارتفاع بشكل مستقل قد يكسر aspect ratio.
الخطأ الرابع: الكتابة مباشرة فوق الصورة الأصلية
ذلك يجعل تعديل watermark لاحقًا أصعب.
الخطأ الخامس: استخدام PNG ضخم جدًا
هذا يؤدي إلى استهلاك ذاكرة وحجم ملف غير ضروري.
أخطاء شائعة في PDF
الخطأ الأول: إدراج صورة watermark جديدة لكل صفحة
في ملف كبير قد يكون من الأفضل إعادة استخدام الصورة نفسها عبر XREF. توثيق PyMuPDF يوفر لهذه الحالة دعم xref.
الخطأ الثاني: استخدام صفحة PDF كأنها صورة
إذا كان watermark متجهيًا، قد يكون show_pdf_page() أنسب من تحويله إلى PNG.
الخطأ الثالث: تجاهل Orientation
بعض المستندات تحتوي على صفحات ذات دوران أو هندسة مختلفة. اختبر المستندات الواقعية وليس PDF A4 عمودي فقط.
الخطأ الرابع: تجاهل حجم الملف النهائي
أحيانًا تعمل المعالجة بشكل صحيح لكن حجم PDF يتضاعف عدة مرات. هنا يجب فحص إعدادات الصور وإعادة استخدام الموارد وخيارات الحفظ.
Watermark على PDF بلوجو شفاف مع حساب الحجم
لنكتب نسخة عملية أكثر احترافية:
import pymupdf
def add_logo_to_pdf(
source_pdf,
destination_pdf,
logo_path,
width_ratio=0.16,
margin=25,
):
doc = pymupdf.open(
source_pdf
)
xref = 0
for page in doc:
page_rect = page.rect
logo_width = (
page_rect.width
* width_ratio
)
logo_height = (
logo_width
* 0.35
)
x0 = (
page_rect.width
- logo_width
- margin
)
y0 = (
page_rect.height
- logo_height
- margin
)
logo_rect = pymupdf.Rect(
x0,
y0,
x0 + logo_width,
y0 + logo_height,
)
if xref == 0:
xref = page.insert_image(
logo_rect,
filename=logo_path,
keep_proportion=True,
overlay=True,
)
else:
page.insert_image(
logo_rect,
xref=xref,
keep_proportion=True,
overlay=True,
)
doc.save(
destination_pdf,
garbage=4,
deflate=True,
)
doc.close()ملاحظة مهمة: logo_height = logo_width * 0.35 هنا مجرد تقدير تقريبي للنسبة، وليس من الأفضل افتراض هذه النسبة لأي شعار. في تطبيق عام، يمكن قراءة أبعاد الصورة فعليًا ثم بناء المستطيل وفق النسبة الحقيقية.
بناء دالة لحساب مستطيل Watermark في PDF
يمكننا تحسين ذلك:
from PIL import Image
def get_watermark_rect(
page,
image_path,
width_ratio=0.16,
margin=25,
):
page_rect = page.rect
with Image.open(
image_path
) as watermark:
image_width = watermark.width
image_height = watermark.height
target_width = (
page_rect.width
* width_ratio
)
ratio = (
target_width
/ image_width
)
target_height = (
image_height
* ratio
)
x0 = (
page_rect.width
- target_width
- margin
)
y0 = (
page_rect.height
- target_height
- margin
)
return pymupdf.Rect(
x0,
y0,
x0 + target_width,
y0 + target_height,
)بهذا لم نعد نفترض aspect ratio.
إنشاء مكتبة صغيرة خاصة بك
إذا تكرر هذا المشروع في عدة تطبيقات، يمكنك وضعه في package:
python_watermark/
│
├── python_watermark/
│ ├── __init__.py
│ ├── image.py
│ ├── pdf.py
│ ├── geometry.py
│ └── utils.py
│
├── tests/
│ ├── test_image.py
│ └── test_pdf.py
│
├── pyproject.toml
└── README.mdفي image.py:
def add_text_watermark(...):
...وفي pdf.py:
def add_image_watermark(...):
...وفي geometry.py:
def calculate_position(...):
...هذا أفضل بكثير من الاحتفاظ بكل شيء داخل:
main.pyخصوصًا عندما يزداد حجم المشروع.
الاختبارات الآلية
يمكن استخدام pytest:
pip install pytestمثال:
from pathlib import Path
from PIL import Image
def test_watermark_output_exists(tmp_path):
output = (
tmp_path
/ "watermarked.jpg"
)
add_text_watermark(
"tests/assets/photo.jpg",
output,
"example.com",
"tests/assets/font.ttf",
)
assert output.exists()ثم:
pytestيمكن إضافة اختبار أبعاد:
with Image.open(output) as image:
assert image.width > 0
assert image.height > 0واختبار أن الملف قابل للفتح.
اختبار PDF
import pymupdf
def test_pdf_watermark(tmp_path):
output = (
tmp_path
/ "watermarked.pdf"
)
watermark_pdf(
"tests/assets/input.pdf",
output,
"tests/assets/watermark.png",
)
assert output.exists()
doc = pymupdf.open(output)
assert doc.page_count > 0
doc.close()ثم يمكن توسيع الاختبار للتحقق من نص watermark إذا كان إدخال النص مباشرًا.
فصل إعدادات التصميم عن منطق المعالجة
لا تجعل:
fill=(255, 255, 255, 100)منتشرًا في كل مكان.
اجعلها:
DEFAULT_TEXT_COLOR = (
255,
255,
255,
)
DEFAULT_OPACITY = 100ثم:
fill=(
*DEFAULT_TEXT_COLOR,
DEFAULT_OPACITY,
)هذا يجعل إعادة تصميم الـ watermark أسهل.
دعم ألوان مختلفة
يمكن تعريف:
def parse_rgb(
value,
):
parts = [
int(part.strip())
for part in value.split(",")
]
if len(parts) != 3:
raise ValueError(
"RGB must have 3 values"
)
return tuple(parts)ثم:
color = parse_rgb(
"255,255,255"
)في API حقيقي يفضل استقبال JSON مثل:
{
"color": [255, 255, 255],
"opacity": 100
}جعل API مرنًا
يمكن أن تكون الطلبات:
{
"type": "text",
"text": "example.com",
"position": "bottom-right",
"opacity": 100
}أو:
{
"type": "logo",
"position": "bottom-left",
"opacity": 120
}أو:
{
"type": "diagonal",
"text": "CONFIDENTIAL",
"opacity": 70
}من الأفضل بناء schema واضح بدل تمرير أي قيمة إلى المعالجة.
Watermark كجزء من Pipeline للصور
في تطبيق كبير قد يكون لديك:
Upload
↓
Validate
↓
Normalize Orientation
↓
Resize
↓
Watermark
↓
Optimize
↓
Save
↓
CDNوهذا أفضل من النظر إلى watermark كعملية منفصلة.
ترتيب العمليات يمكن أن يؤثر على النتيجة. مثلًا إذا أضفت watermark ثم قمت بتصغير الصورة بشكل كبير، فقد يصبح watermark أصغر من اللازم. لذلك غالبًا يجب اتخاذ قرار واضح بشأن حجم الصورة النهائي قبل حساب حجم العلامة المائية.
Watermark كجزء من Pipeline للـ PDF
يمكن أن يكون:
Upload PDF
↓
Validate
↓
Open with PyMuPDF
↓
For each page
↓
Calculate placement
↓
Insert watermark
↓
Save optimized PDF
↓
Verify output
↓
Return fileإذا كانت الخدمة تعالج ملفات حساسة، يمكن إضافة خطوة Audit Log:
User ID
Document ID
Watermark ID
Time
Output hashلكن هذه البيانات يجب أن تكون جزءًا من تصميم النظام، وليست داخل watermark نفسه بالضرورة.
إنشاء Hash للملف النهائي
يمكن حساب SHA-256:
import hashlib
def sha256_file(path):
sha256 = hashlib.sha256()
with open(path, "rb") as file:
for chunk in iter(
lambda: file.read(1024 * 1024),
b"",
):
sha256.update(chunk)
return sha256.hexdigest()ثم:
digest = sha256_file(
"output/document.pdf"
)
print(digest)هذا مفيد في أنظمة التتبع والتخزين، خصوصًا عندما تريد التأكد من أن الملف الذي تم إنتاجه هو نفسه الذي تم تنزيله أو تخزينه لاحقًا.
ماذا عن إزالة العلامة المائية؟
يجب أن نفرق بين watermark مبني على الصورة وبين watermark نصي داخل PDF.
إذا كانت العلامة المائية جزءًا من البكسلات بعد حفظ الصورة، فإزالتها بشكل مثالي غالبًا تتطلب استعادة المحتوى الذي كان أسفلها، وهذا ليس مجرد "حذف طبقة".
أما في بعض ملفات PDF، فإن العناصر قد تظل كعناصر منفصلة داخل بنية المستند، لكن حذفها يتطلب معرفة دقيقة بكيفية إنشاء المستند وبنية المحتوى، ولا ينبغي بناء نظام يفترض أن كل watermark يمكن التراجع عنه تلقائيًا.
لهذا السبب، إذا كنت تحتاج إلى نسخة أصلية، احتفظ بالأصل بدل الاعتماد على فكرة إزالة watermark لاحقًا.
أفضل استراتيجية عملية للمشاريع الحقيقية
بعد كل هذه الأمثلة، يمكن تلخيص التصميم الجيد في قاعدة بسيطة:
الصورة الأصلية تبقى منفصلة.
الـ watermark يتم توليده أو تحميله من مصدر موثوق.
الـ opacity وposition وscale تكون قابلة للتكوين.
النتيجة تحفظ بامتداد يناسب المحتوى.
الـ PDF يعالج باستخدام مكتبة PDF فعلية بدل تحويل كل شيء إلى صور بلا داعٍ.
الموارد المتكررة يعاد استخدامها.
الملفات الكبيرة لها حدود واضحة.
كل عملية لديها logging.
الـ API يتحقق من الملفات قبل معالجتها.
والاختبارات تغطي الحالات الواقعية وليس السيناريو المثالي فقط.
مشروع نهائي كامل للصور
لنضع نسخة متماسكة من كل الفكرة:
from pathlib import Path
from PIL import (
Image,
ImageDraw,
ImageFont,
ImageOps,
)
def calculate_position(
image_size,
item_size,
position,
margin,
):
width, height = image_size
item_width, item_height = item_size
positions = {
"top-left": (
margin,
margin,
),
"top-center": (
(width - item_width) // 2,
margin,
),
"top-right": (
width - item_width - margin,
margin,
),
"center": (
(width - item_width) // 2,
(height - item_height) // 2,
),
"bottom-left": (
margin,
height - item_height - margin,
),
"bottom-center": (
(width - item_width) // 2,
height - item_height - margin,
),
"bottom-right": (
width - item_width - margin,
height - item_height - margin,
),
}
try:
return positions[position]
except KeyError:
raise ValueError(
f"Unknown position: {position}"
)
def add_text_watermark(
input_path,
output_path,
text,
font_path,
font_size=48,
opacity=100,
color=(255, 255, 255),
position="bottom-right",
margin=40,
):
image = Image.open(
input_path
)
image = ImageOps.exif_transpose(
image
)
image = image.convert(
"RGBA"
)
overlay = Image.new(
"RGBA",
image.size,
(0, 0, 0, 0),
)
draw = ImageDraw.Draw(
overlay
)
font = ImageFont.truetype(
font_path,
font_size,
)
bbox = draw.textbbox(
(0, 0),
text,
font=font,
)
item_width = (
bbox[2] - bbox[0]
)
item_height = (
bbox[3] - bbox[1]
)
x, y = calculate_position(
image.size,
(item_width, item_height),
position,
margin,
)
draw.text(
(x, y),
text,
font=font,
fill=(
color[0],
color[1],
color[2],
opacity,
),
)
result = Image.alpha_composite(
image,
overlay,
)
suffix = (
Path(output_path)
.suffix
.lower()
)
if suffix in {
".jpg",
".jpeg",
}:
result = result.convert(
"RGB"
)
result.save(
output_path,
quality=90,
optimize=True,
)
elif suffix == ".png":
result.save(
output_path,
optimize=True,
)
else:
raise ValueError(
"Only JPG and PNG are supported"
)
if __name__ == "__main__":
add_text_watermark(
input_path="input/photo.jpg",
output_path=(
"output/photo_watermarked.jpg"
),
text="example.com",
font_path="fonts/DejaVuSans.ttf",
font_size=48,
opacity=100,
position="bottom-right",
)هذه النسخة أفضل من الأمثلة الصغيرة لأنها تحتوي على تصحيح EXIF وحساب الموقع والتحكم بالشفافية ودعم JPG وPNG.
مشروع نهائي كامل للـ PDF
والآن نسخة متماسكة للـ PDF:
import pymupdf
def watermark_pdf(
input_pdf,
output_pdf,
watermark_image,
width_ratio=0.30,
center=True,
overlay=True,
):
doc = pymupdf.open(
input_pdf
)
xref = 0
for page in doc:
page_rect = page.rect
target_width = (
page_rect.width
* width_ratio
)
target_height = (
target_width
* 0.5
)
if center:
x0 = (
page_rect.width
- target_width
) / 2
y0 = (
page_rect.height
- target_height
) / 2
else:
x0 = 30
y0 = (
page_rect.height
- target_height
- 30
)
rect = pymupdf.Rect(
x0,
y0,
x0 + target_width,
y0 + target_height,
)
if xref == 0:
xref = page.insert_image(
rect,
filename=watermark_image,
keep_proportion=True,
overlay=overlay,
)
else:
page.insert_image(
rect,
xref=xref,
keep_proportion=True,
overlay=overlay,
)
doc.save(
output_pdf,
garbage=4,
deflate=True,
)
doc.close()
if __name__ == "__main__":
watermark_pdf(
input_pdf="input/document.pdf",
output_pdf=(
"output/document_watermarked.pdf"
),
watermark_image=(
"watermark/logo.png"
),
width_ratio=0.30,
)هذه الدالة مناسبة كنقطة انطلاق حقيقية ويمكن تطويرها بسهولة لدعم:
position
margin
opacity
specific pages
custom sizes
text
logo
customer IDكيف تختار بين Text Watermark وLogo Watermark؟
إذا كان المطلوب هو Branding، استخدم الشعار.
إذا كان المطلوب هو حالة المستند، استخدم نصًا واضحًا مثل DRAFT.
إذا كان المطلوب هو ربط الملف بعميل، استخدم نصًا يحتوي على اسم العميل أو معرفه.
إذا كان المطلوب هو منع الاستخدام غير الرسمي بصريًا، قد يناسب watermark قطري كبير وشفاف.
إذا كان المطلوب هو الحصول على تصميم ثابت ومعقد، أنشئ watermark كصورة أو PDF مستقل ثم أدرجه.
وفي كثير من المشاريع يمكن الجمع بين أكثر من نوع.
مثال عملي لنظام مستندات تجاري
لنفترض أنك تبني منصة SaaS تصدر تقارير PDF.
عندما يضغط العميل:
Generate Reportيحدث:
Generate PDF
↓
Add Company Logo
↓
Add Customer Name
↓
Add Document ID
↓
Add "CONFIDENTIAL"
↓
Save
↓
Return Downloadيمكن أن يكون لكل جزء وظيفة مختلفة.
الشعار هو Branding.
اسم العميل هو تخصيص.
المعرف هو Tracking.
كلمة CONFIDENTIAL هي حالة بصرية.
هذه الطريقة تعطي للمستخدم مستندًا يبدو مصممًا لهذا النظام وليس مجرد PDF عادي خرج من مكتبة.
نقطة مهمة عن الأداء في Web Applications
إذا كان لديك endpoint يستقبل:
50 MB PDFويعالج:
300 pagesفلا تتعامل معه مثل:
image.jpg
2 MBقد تحتاج إلى:
background task
file size limits
temporary storage
streaming
job queue
logging
retry
timeout policiesوبالمثل، إذا كانت لديك صور متعددة بدقة عالية، فقد يكون Celery أو نظام queues أفضل بكثير من تنفيذ كل العمليات داخل request.
متى تكون Pillow وحدها كافية؟
إذا كان المطلوب فقط:
JPG
PNG
WebPمع:
text watermark
logo
opacity
position
resizeفـ Pillow غالبًا كافية.
لا تحتاج إلى مكتبة PDF إذا لم يكن لديك PDF أصلًا.
متى تحتاج PyMuPDF؟
عندما يكون لديك:
PDFوتريد:
add image
add text
inspect pages
render pages
modify content
save optimized filePyMuPDF مناسب لهذا النوع من العمل، ووثائقه الحالية تعرض APIs مخصصة للصفحات والصور والنصوص والتعامل مع المستندات.
متى تستخدم ReportLab بدل PyMuPDF؟
ReportLab قوية جدًا عندما تريد إنشاء PDF من الصفر.
مثلًا:
Build invoice
↓
Draw logo
↓
Draw table
↓
Draw footer
↓
Save PDFلكن إذا كان لديك PDF موجود أصلًا وتريد إضافة watermark فوق صفحاته، فإن PyMuPDF يقدم API مباشرة لهذا الاستخدام عبر الصفحة نفسها، وهو سيناريو يتطابق بوضوح مع وظائف insert_image() وshow_pdf_page() وواجهات إدخال النص.
لذلك يمكن أن يكون الاختيار المعماري:
Create new PDF
→ ReportLab
Modify existing PDF
→ PyMuPDFمع أن المكتبات تستطيع في بعض الحالات تغطية أجزاء متداخلة من الاستخدامات.
أفضل بنية لمشروع Production
يمكن أن تكون:
app/
│
├── watermark/
│ ├── image.py
│ ├── pdf.py
│ ├── text.py
│ ├── position.py
│ └── service.py
│
├── api/
│ └── watermark.py
│
├── tasks/
│ └── watermark_tasks.py
│
├── fonts/
│ └── ...
│
├── templates/
│ └── ...
│
└── tests/
├── test_image.py
├── test_pdf.py
└── assets/هذا يجعل المشروع قابلًا للنمو.
قائمة تحقق قبل إطلاق النظام
قبل نقل أداة watermark إلى الإنتاج، اختبر ما يلي:
هل يمكن فتح الملف الأصلي؟
هل حجم الملف ضمن الحدود؟
هل الصورة ضخمة بشكل غير منطقي؟
هل الخط موجود؟
هل watermark يظهر في الموضع الصحيح؟
هل opacity مناسبة؟
هل Aspect Ratio محفوظ؟
هل PDF النهائي يمكن فتحه؟
هل جميع الصفحات عولجت؟
هل حجم الملف النهائي منطقي؟
هل الملفات المؤقتة تُحذف؟
هل الأخطاء تسجل في logs؟
هل الملفات الأصلية تبقى محفوظة عندما يكون ذلك مطلوبًا؟
هل التطبيق يمنع المستخدم من تمرير مسارات ملفات غير آمنة؟
هل توجد حدود للذاكرة والزمن؟
هذه القائمة تبدو بسيطة، لكن كثيرًا من المشاكل تظهر فقط عندما ينتقل المشروع من ملف واحد إلى آلاف الملفات.
نصائح عملية أخيرة من واقع بناء أدوات معالجة الملفات
أكبر خطأ يمكن أن ترتكبه هو جعل watermark ثابتًا جدًا. عندما يكون كل شيء مبنيًا على صورة بحجم واحد ومقاس واحد وخط واحد، ستكتشف سريعًا أن الصور الحقيقية مختلفة عن الصور التجريبية.
ابدأ منذ البداية بدعم النسب المئوية للمقاسات، والمواقع المنطقية، والشفافية، والخطوط الخارجية، ثم اجعل تلك الإعدادات قابلة للتكوين.
ولا تضع كل منطق النظام داخل endpoint. اجعل المعالجة في service مستقل. هذا يجعل الاختبار أسهل، ويسمح لك باستعمال نفس الكود داخل Django أو Flask أو FastAPI أو CLI أو Celery.
وفي PDF، فكّر في حجم الملف قبل أن تفكر فقط في الشكل النهائي. العلامة المائية الجميلة ليست مفيدة كثيرًا إذا كان إدراجها يجعل ملفًا صغيرًا يتحول إلى ملف ضخم جدًا بلا سبب.
وفي الصور، لا تنسَ أن الجودة النهائية تعتمد على ترتيب العمليات. إذا كنت ستغير حجم الصورة، فعل ذلك قبل أن تحدد حجم watermark المناسب. وإذا كنت ستضغط الصورة بشدة، اختبر أثر الضغط على النص الصغير.
ومن الناحية الأمنية، تعامل مع الملفات المرفوعة على أنها بيانات غير موثوقة. تحقق منها وحدد حجمها وخزنها بأسماء آمنة ولا تجعل مسار التخزين يعتمد مباشرة على اسم المستخدم.
الخلاصة
إضافة العلامة المائية إلى الصور وملفات PDF باستخدام Python ليست عملية معقدة في جوهرها، لكن بناءها بطريقة احترافية يتطلب أكثر من مجرد سطرين من الكود. في الصور، توفر Pillow بيئة مرنة جدًا لإنشاء طبقات شفافة باستخدام RGBA، ورسم النصوص والشعارات، وتغيير الحجم، وتدوير الطبقات، ثم دمجها مع الصورة الأصلية. توثيق Pillow يوضح بوضوح دور Alpha وقنوات RGBA وطرق رسم العناصر الشفافة ودمجها.
أما في ملفات PDF، فإن PyMuPDF يقدم طريقة مباشرة للتعامل مع الصفحات وإضافة صورة watermark بواسطة Page.insert_image()، كما يدعم إعادة استخدام الصورة عبر XREF من أجل تحسين التعامل مع الملفات متعددة الصفحات. ويمكن أيضًا إدراج النصوص بشكل مباشر باستخدام insert_text() أو insert_textbox() مع إعدادات للشفافية والخط والمحاذاة.
القاعدة الأساسية التي أنصح بها هي أن تجعل watermark مكوّنًا مستقلاً داخل تطبيقك. لا تكتب منطقًا خاصًا بملف واحد، بل ابنِ خدمة يمكن أن تستقبل صورة أو PDF وإعدادات مثل:
text
logo
opacity
position
scale
margin
font
pagesثم تجعل الخدمة تنتج ملفًا جديدًا.
عندما تصل إلى هذه البنية، تصبح إضافة ميزات جديدة سهلة جدًا. يمكنك بعد ذلك إضافة watermark مخصص لكل عميل، أو watermark قطري، أو رقم نسخة، أو شعار في الـ footer، أو اسم المستخدم، أو معرف وثيقة، أو معالجة آلاف الملفات باستخدام queue system مثل Celery.
والأجمل في Python أن البداية صغيرة جدًا. يمكنك أن تبدأ بـ:
from PIL import Image
image = Image.open("photo.jpg")ثم بعد ذلك تنتقل إلى طبقة RGBA، ثم إلى دالة Watermark قابلة لإعادة الاستخدام، ثم إلى معالجة batch، ثم إلى API، ثم إلى PDF بواسطة PyMuPDF. أي أن المشروع يمكن أن يبدأ كسكربت صغير جدًا، ثم يتحول تدريجيًا إلى خدمة كاملة دون الحاجة إلى تغيير الفكرة الأساسية.
وفي النهاية، تذكر أن العلامة المائية وسيلة للهوية والتعريف والتتبع البصري، وليست بديلًا عن أنظمة الحماية الحقيقية. أفضل نظام هو الذي يجمع بين watermark مناسب، وتخزين آمن، وصلاحيات وصول، والتحقق من الملفات، وإدارة جيدة للبيانات، واختبارات مستمرة. عندما تجمع هذه العناصر معًا، تصبح عملية معالجة الصور وPDF باستخدام Python جزءًا قويًا وعمليًا من أي تطبيق حديث يتعامل مع المحتوى الرقمي.