# معالجة الصور بمكتبة GD الخام عندما لا يتوفر ImageMagick

- آدرس: https://di9.ir/blog/ar/%D9%85%D8%B9%D8%A7%D9%84%D8%AC%D8%A9-%D8%A7%D9%84%D8%B5%D9%88%D8%B1-%D8%A8%D9%85%D9%83%D8%AA%D8%A8%D8%A9-gd-%D8%A8%D8%AF%D9%88%D9%86-imagemagick
- نویسنده: danidiba
- انتشار: 2026-08-08T01:29:05+03:30
- به‌روزرسانی: 2026-08-08T01:29:05+03:30
- دسته: الاستضافة والخوادم
- برچسب‌ها: PHP، بهینه‌سازی، هاست اشتراکی

> على الاستضافة المشتركة التي تحجب تنفيذ أوامر النظام تكون GD هي الخيار الوحيد. ثلاث حمايات بدونها ينهار مسار معالجة الصور بصمت، ولماذا يجب أن تختلف جودة WebP للقطات الشاشة.

على الاستضافة المشتركة تكون دوال تنفيذ أوامر النظام محجوبة عادة. أي أن أي حل للصور يعتمد على استدعاء برنامج تنفيذي خارجي غير متاح لك. شرحنا هذا القيد بالكامل في [مقال disable_functions](https://di9.ir/blog/disable-functions-%D9%87%D8%A7%D8%B3%D8%AA-%D8%A7%D8%B4%D8%AA%D8%B1%D8%A7%DA%A9%DB%8C).

إذن تبقى GD: المكتبة المدمجة في PHP نفسها. هذا المقال هو مسار المعالجة الذي بنيناه بـGD لهذا الموقع، مع ثلاث حمايات بدونها ينهار العمل بصمت.

## أولا: انظر ما الذي لديك

```php
var_dump(extension_loaded('gd'));
var_dump(function_exists('imagewebp'));
var_dump(function_exists('imagecreatefromavif'));
print_r(gd_info());
```

ملاحظة: **وجود الإضافة لا يعني أن كل الصيغ مدعومة.** في بعض البنى يتوفر WebP دون AVIF أو العكس. تحقق دائما من الدالة المحددة لا من الإضافة وحدها.

## الحماية الأولى: الذاكرة قبل فتح الملف

هذا أهم قسم في المقال.

عندما تفتح GD ملف JPEG فإنها تحوّله إلى مصفوفة بكسلات. والذاكرة اللازمة **تعتمد على عدد البكسلات لا على حجم الملف**. قد تكون صورة JPEG حجمها ميغابايت واحد بدقة اثني عشر ميغابكسل، وتستهلك عند فتحها عشرات أضعاف حجم ملفها من الذاكرة.

والنتيجة أسوأ أنواع الفشل: تموت العملية دون أي رسالة مفهومة. يضغط المستخدم الزر، وتصبح الصفحة بيضاء، ولا يُسجَّل أي خطأ في أي مكان.

الحل: **قدّر قبل الفتح، وارفض صراحة إذا لم تتسع الذاكرة.**

```php
private static function assertCanOpen(string $path): void
{
    $info = @getimagesize($path);

    if ($info === false) {
        throw new RuntimeException('ملف الصورة غير قابل للقراءة.');
    }

    [$width, $height] = $info;
    $channels = $info['channels'] ?? 4;
    $bits = $info['bits'] ?? 8;

    // تقدير متحفظ مع معامل أمان
    $needed = (int) ($width * $height * ($bits / 8) * $channels * 1.8);
    $limit = self::memoryLimitBytes();
    $free = $limit - memory_get_usage(true);

    if ($limit > 0 && $needed > $free) {
        throw new RuntimeException('أبعاد الصورة أكبر من الذاكرة المتاحة.');
    }
}
```

الدالة `getimagesize` لا تقرأ سوى ترويسة الملف ولا تستهلك تقريبا أي ذاكرة، لذا فهذا الفحص آمن.

وقراءة سقف الذاكرة لها عدة حالات يجب أن تغطيها كلها:

```php
private static function memoryLimitBytes(): int
{
    $raw = trim((string) ini_get('memory_limit'));

    if ($raw === '' || $raw === '-1') {
        return 0; // بلا سقف
    }

    $unit = strtolower(substr($raw, -1));
    $value = (int) $raw;

    return match ($unit) {
        'g' => $value * 1024 * 1024 * 1024,
        'm' => $value * 1024 * 1024,
        'k' => $value * 1024,
        default => $value,
    };
}
```

## الحماية الثانية: تدوير EXIF

صور الهاتف تُحفظ عادة بوضع أفقي، ويُكتب الاتجاه الصحيح في بيانات EXIF الوصفية. المتصفح يقرأ هذه البيانات، **أما GD فلا**.

النتيجة: صورة تبدو سليمة في معرض الهاتف تخرج مقلوبة بعد المعالجة.

```php
private static function applyExifRotation($image, string $path)
{
    if (! function_exists('exif_read_data')) {
        return $image;
    }

    $exif = @exif_read_data($path);
    $orientation = (int) ($exif['Orientation'] ?? 1);

    return match ($orientation) {
        3 => imagerotate($image, 180, 0),
        6 => imagerotate($image, -90, 0),
        8 => imagerotate($image, 90, 0),
        default => $image,
    };
}
```

وله أثر جانبي إيجابي أيضا: ناتج GD لا يحمل البيانات الوصفية، لذا **تُحذف تلقائيا الإحداثيات الجغرافية واسم الجهاز المخزَّنان في صورة الهاتف.** وهذه ميزة جيدة لخصوصية المستخدمين.

## الحماية الثالثة: الشفافية

إذا غيّرت حجم صورة PNG أو WebP شفافة بالإعدادات الافتراضية فستتحول الخلفية الشفافة إلى سوداء. يلزم سطران، ومن السهل نسيانهما:

```php
$canvas = imagecreatetruecolor($newWidth, $newHeight);

imagealphablending($canvas, false);
imagesavealpha($canvas, true);

$transparent = imagecolorallocatealpha($canvas, 0, 0, 0, 127);
imagefilledrectangle($canvas, 0, 0, $newWidth, $newHeight, $transparent);

imagecopyresampled(
    $canvas, $source,
    0, 0, 0, 0,
    $newWidth, $newHeight, $srcWidth, $srcHeight
);
```

استخدم دائما `imagecopyresampled` لا `imagecopyresized`؛ فالثانية لا تُجري الاستيفاء (interpolation) وتبدو نتيجتها متدرجة بخطوات حادة.

## اختر الجودة بحسب نوع الصورة

لم نجد هذه النقطة في أي شرح، لكنها حيوية لمدونة تقنية.

صورك نوعان، و**حاجة كل منهما إلى الجودة ليست واحدة**:

- **الصورة الفوتوغرافية** — تدرجات ناعمة بلا حواف حادة. تتحمل الضغط مع الفقد جيدا.
- **لقطات الشاشة والرسوم** — نص صغير، وخطوط بسمك بكسل واحد، وحواف حادة. الضغط نفسه هنا يشوّه الحروف، ويصبح الكود داخل الصورة غير مقروء.

إذا كانت مدونتك مليئة بلقطات شاشة الطرفية — مثل هذه المدونة — فإن جودة واحدة للجميع تعني إما حجما زائدا بلا سبب أو نصا مطموسا. وضعنا مستويين منفصلين للجودة وتركنا الاختيار لمن يرفع الصورة:

```php
$quality = $isScreenshot ? 93 : 82;
imagewebp($canvas, $destination, $quality);
```

اضبط الأرقام على صورك الفعلية؛ المهم أن **يكون لديك مستويان**، لا أن تكون الأرقام هذه بالضبط.

## النسخ المتعددة وsrcset

أنشئ لكل صورة عدة عروض حتى يحصل الهاتف على ملف صغير:

```php
$widths = [400, 800, 1200, 1600];

foreach ($widths as $width) {
    if ($width >= $sourceWidth) {
        continue; // لا تكبير أبدا
    }

    // إنشاء النسخة وحفظها
}
```

**لا تكبّر الصورة.** إنشاء نسخة بعرض 1600 من مصدر عرضه 800 لا يضيف إلا حجما زائدا بلا أي فائدة في الجودة.

### الفخ الذي وقعنا فيه

للقاعدة أعلاه عارض جانبي لم نلحظه فترة. افترض أن عرض المصدر 784 بكسل:

- نسخ WebP: تُنشأ نسخة 400 فقط، لأن 800 أكبر من المصدر.
- النسخة الاحتياطية JPEG: تُنشأ بالعرض الأصلي أي 784.

والنتيجة؟ **المتصفح الحديث الذي يفهم WebP يحصل على صورة 400، والمتصفح القديم على صورة 784.** أي كلما كان المتصفح أفضل كانت الصورة أضعف.

الحل: عندما يكون المصدر أضيق من أكبر عرض في القائمة، **أضف العرض الأصلي أيضا إلى نسخ WebP.**

### في جانب HTML

```html
<picture>
  <source type="image/webp" srcset="a-400.webp 400w, a-800.webp 800w" sizes="(max-width: 48rem) 100vw, 48rem">
  <img src="a.jpg" width="1200" height="800" alt="..." loading="lazy" decoding="async">
</picture>
```

**اكتب `width` و`height` دائما.** بدونهما لا يعرف المتصفح الارتفاع فتقفز الصفحة أثناء تحميل الصورة. وهذا أحد المقاييس الثلاثة الرئيسية لتجربة الصفحة لدى Google، وإصلاحه مجاني.

لا تضع `loading="lazy"` على أول صورة في الصفحة؛ فهي عادة أكبر عنصر مرئي، وتأخير تحميلها يزيد السرعة سوءا.

## دعم AVIF كمدخل

إذا رفع المستخدم ملف AVIF وكنت لا تعرف سوى JPEG وPNG فسيحصل على خطأ غامض. من الأفضل أن تقبل المدخل حتى لو كنت لا تُنتج AVIF:

```php
$image = match ($mime) {
    'image/jpeg' => imagecreatefromjpeg($path),
    'image/png' => imagecreatefrompng($path),
    'image/webp' => imagecreatefromwebp($path),
    'image/avif' => function_exists('imagecreatefromavif')
        ? imagecreatefromavif($path)
        : throw new RuntimeException('هذا الخادم لا يدعم مدخلات AVIF.'),
    default => throw new RuntimeException('صيغة الصورة غير مدعومة.'),
};
```

نتعمّد عدم إنتاج AVIF: على الاستضافة المشتركة المعالج مكلف، وفائدته مقارنة بـWebP هامشية.

## أفضل من الرفض: التصغير

كانت النسخة الأولى من مسارنا ترفض الصور التي تتجاوز الحد. وكانت تجربة مستخدم سيئة: يرفع مدير الموقع صورة عادية من الهاتف فيتلقى رسالة خطأ.

النسخة الأفضل: **إذا تجاوزت الأبعاد السقف فصغّرها تلقائيا وتابع.** ولا ترفض إلا عندما لا تتسع في الذاكرة حتى بعد التصغير.

## التحقق من الملف: لا تثق بالامتداد

قبل أي معالجة افحص الملف الوارد بجدية. امتداد الملف وحتى النوع الذي يعلنه المتصفح **كلاهما قابل للتزوير**.

```php
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($path);

$allowed = ['image/jpeg', 'image/png', 'image/webp', 'image/avif'];

if (! in_array($mime, $allowed, true)) {
    throw new RuntimeException('صيغة الملف غير مسموح بها.');
}
```

والأهم: **أنشئ اسم الملف النهائي بنفسك**، ولا تستخدم اسم المستخدم المُرسَل مباشرة أبدا. قد يحتوي الاسم على محارف مسار، أو يحاول صاحبه إنشاء ملف تنفيذي بامتداد مزدوج.

النمط الذي نستخدمه: slug عربي من عنوان الصورة، يليه تجزئة (hash) عشوائية قصيرة لضمان التفرد، يليها امتداد حددناه **نحن** بحسب صيغة الناتج.

وقسّم مجلد الوجهة بحسب السنة والشهر:

```
storage/app/public/media/2025/05/صورة-نموذجية-a1b2c3-1200.webp
```

والسبب أداء نظام الملفات: مجلد فيه عشرات الآلاف من الملفات يبطئ كل عملية سرد للمحتويات.

## الفحص في المتصفح: تجربة مستخدم لا أمان

التحقق من جهة الخادم إلزامي. لكن إذا لم يكن لديك سواه فسيضطر المستخدم إلى رفع ملف حجمه عشرة ميغابايت كاملا ليكتشف أنه غير مقبول. وعلى اتصال بطيء يعني هذا انتظار دقائق لرسالة خطأ.

ببضعة أسطر من JavaScript الخام يمكن الفحص قبل الإرسال:

```javascript
input.addEventListener('change', () => {
  const file = input.files[0];
  if (!file) return;

  if (file.size > 8 * 1024 * 1024) {
    show('حجم الملف أكبر من ثمانية ميغابايت.');
    input.value = '';
    return;
  }

  const img = new Image();
  img.onload = () => {
    if (img.naturalWidth * img.naturalHeight > 40_000_000) {
      show('أبعاد الصورة كبيرة جدا.');
      input.value = '';
    }
    URL.revokeObjectURL(img.src);
  };
  img.src = URL.createObjectURL(file);
});
```

وأضف شريط تقدم حقيقيا أيضا. عند رفع عدة ميغابايت على اتصال بطيء، غياب التغذية الراجعة يعني أن المستخدم يظن أن الموقع علق فيضغط الزر مرة أخرى.

**هذه الفحوص ليست بديلا عن التحقق في الخادم.** كل ما يُنفَّذ في المتصفح يمكن تجاوزه.

## ما الذي لا تقترب منه

هناك أمور ممكنة بـGD لكن لا ينبغي أن تقترب منها على الاستضافة المشتركة:

- **معالجة عدة صور كبيرة في الوقت نفسه ضمن طلب واحد.** ذاكرة كل صورة لا تُحرَّر ما لم تستدعِ `imagedestroy` صراحة، وحتى حينها قد يسبب تجزؤ الذاكرة مشكلة. عالج الصور واحدة تلو الأخرى.
- **إنشاء النسخ لحظة الطلب.** أي معالجة صورة مع كل زيارة. أنشئ النسخ **عند الرفع**، ثم قدّم الملف الثابت فقط.
- **العمل على صور ضخمة جدا.** ضع سقفا منطقيا وصغّر ما يتجاوزه.

وهذه الثلاثة ترجع إلى مبدأ واحد: على الاستضافة المشتركة، **يجب أن يُنفَّذ العمل الثقيل مرة واحدة وتُحفظ نتيجته** — لا أن يتكرر مع كل زيارة.

## أداة يجب أن تبنيها

إنشاء نسخ الصور يتغير مع الوقت: تضيف عرضا جديدا، وتغيّر الجودة. يستحق الأمر وجود أمر يعيد بناء كل النسخ من المصدر دون الحاجة إلى إعادة الرفع:

```bash
php84 artisan media:reprocess --id=42
php84 artisan media:reprocess --all
```

احتفظ دائما بالملف المصدر. بدون المصدر يعني أي تغيير في سياسة النسخ إعادة رفع كل شيء.

## القياس: شاهد قبل وبعد فعلا

بدون القياس يكون أي قرار بشأن الجودة تخمينا. بعد كل تغيير في الإعدادات، انظر إلى الحجم الفعلي للمخرجات:

```bash
find ~/app/storage/app/public/media -name '*.webp' -printf '%s %p\n' | sort -rn | head -20
```

يعرض عشرين ملفا من الأكبر. فإذا كان أحدها أكبر بكثير من الباقي، فهذا يعني عادة أن صورة بأبعاد غير معتادة أو بجودة خاطئة عولجت.

وللمساحة الإجمالية المشغولة:

```bash
du -sh ~/app/storage/app/public/media
```

على الاستضافة المشتركة المساحة محدودة، والنسخ المتعددة تضاعفها. فإذا أنشأت لكل صورة أربعة عروض WebP إضافة إلى JPEG احتياطي، فكل رفع يعني خمسة ملفات.

## قرار اتخذناه عن وعي: لا ننتج AVIF

تعطي صيغة AVIF جودة أفضل من WebP عند الحجم نفسه. فلماذا لا ننتجها؟

لأن **كلفة معالجها على الاستضافة المشتركة عالية** وفائدتها الإضافية مقارنة بـWebP هامشية. WebP مدعوم اليوم تقريبا في كل مكان، وفارق حجمه عن AVIF للصور المعتادة على الويب ليس كبيرا بما يستحق وقت المعالجة والتعقيد الإضافي.

لكننا **نقبل AVIF كمدخل**، لأن الهواتف والأدوات الحديثة تنتج بهذه الصيغة بشكل متزايد، ورفضها لا يفعل سوى إرباك المستخدم.

نوصي بنمط القرار هذا: **كن كريما في المدخلات ومتحفظا في المخرجات.** اقرأ كل صيغة تستطيع قراءتها، لكن أنتج فقط الصيغ التي تبرر كلفتها.

## الخلاصة

- قدّر الذاكرة اللازمة قبل فتح الصورة. `getimagesize` رخيصة.
- طبّق تدوير EXIF بنفسك.
- للشفافية لا تنسَ `imagealphablending` و`imagesavealpha`.
- اختر الجودة بحسب نوع الصورة، لا رقما ثابتا للجميع.
- لا تكبّر الصور، لكن انتبه إلى فخ العرض الأصلي.
- اكتب `width` و`height` في HTML.

## پرسش‌های متداول

### هل تستطيع GD إنتاج صور WebP؟

نعم، في إصدارات PHP الحديثة تتوفر دالة إنتاج WebP في GD. تحقق منها بـfunction_exists قبل الاستخدام لأنها معطلة في بعض البنى (builds).

### لماذا يتوقف سكربت معالجة الصور دون رسالة خطأ؟

في الغالب تقريبا بسبب نفاد الذاكرة. فتح الصورة بـGD يستهلك ذاكرة بقدر عدد البكسلات مضروبا في عدة بايتات، لا بقدر حجم الملف المضغوط. لذلك يجب أن تقدّر الحاجة قبل الفتح.

### لماذا تظهر صورة الهاتف مقلوبة بعد المعالجة؟

لأن اتجاه الصورة مخزّن في بيانات EXIF الوصفية ولا تطبقه GD عند الفتح. عليك قراءة الاتجاه وتدوير الصورة بنفسك.

### ما الجودة المناسبة لـWebP؟

تعتمد على نوع الصورة. للصورة الفوتوغرافية العادية يكفي نحو ثمانين، أما للقطات الشاشة والصور التي فيها نص صغير فيجب رفعها وإلا تشوهت حواف الحروف.
