لتحويل NSImageView في تطبيقك إلى صورة نقطية، استخدم طريقتَي التخزين المؤقت للصور النقطية المتكاملتين في AppKit، ثم حافظ على أبعاد البكسل الناتجة. إذا كنت تحتاج إلى بايتات متوقعة لـ Metal، فأعد رسم تلك الصورة النقطية داخل مخزن Core Graphics محدد صراحةً.
يغطي هذا الدليل صورة اصطناعية أُنشئت بالكامل في الذاكرة. الهدف هو الحصول على صورة نقطية قابلة لإعادة الاستخدام لما يعرضه NSImageView، وتشخيص البكسلات الموضوعة في غير مكانها أو المكررة، ونقل الصورة النقطية اختياريًا إلى نسيج Metal.
اختر ما إذا كنت تحتاج إلى العرض الذي يقدمه المنظور أم إلى صورة بحجم محدد تمامًا
للصورة والمنظور الذي يعرضها مسؤوليتان مختلفتان. يطبّق المنظور نمط القياس الخاص به لملاءمة الصورة داخل إطاره. والإعداد الافتراضي هو scaleProportionallyDown، لذا فإن استخراج الصورة الأصلية لا يعيد بالضرورة العرض الذي يقدمه المنظور. يشرح توثيق Apple لخاصية imageScaling هذا السلوك.
اختر الناتج قبل اختيار واجهة البرمجة:
| الناتج المطلوب | نقطة البدء المناسبة | مواصفات الأبعاد |
|---|---|---|
| الصورة كما يرسمها منظور الصورة | طريقتا التخزين المؤقت لصورة المنظور في AppKit | افحص أبعاد البكسل للصورة النقطية المعادة |
| رسم اصطناعي بأبعاد محددة تمامًا | ارسم مباشرةً في CGContext لصورة نقطية |
عرض وارتفاع صحيحان للبكسل صراحةً |
| بايتات متوقعة لنسيج Metal | أعد رسم الصورة النقطية في سياق صورة نقطية محدد | تنسيق بكسل وخطوة صف وملكية محددة صراحةً |
توثّق Apple أن bitmapImageRepForCachingDisplay(in:) ينشئ تمثيلًا مناسبًا لمنطقة المنظور المطلوبة. مرّر هذا التمثيل إلى cacheDisplay(in:to:)؛ وتوصي Apple تحديدًا بهذا الاقتران لضمان التوافق في تنسيق البكسل. ترسم الطريقة الثانية المنظور وعناصره التابعة. توثيق تخصيص الصورة النقطية، توثيق تخزين عرض المنظور مؤقتًا.
لا تتيح هذه الطرق وسيطًا لدقة إخراج اعتباطية. إذا كان شرطك هو الحصول على 192 × 128 بكسل تمامًا، فارسم العمل الاصطناعي مباشرةً بهذا الحجم. لا يمكن لتغيير حجم صورة نقطية أصغر للمنظور لاحقًا استعادة تفاصيل لم تُحوّل إلى بكسلات أصلًا. وهذا نابع من اختلاف مواصفات الناتج بين الطريقتين، وليس من قدرة إضافية للتخزين المؤقت للمنظور.
أبقِ النقاط والبكسلات والمستطيلات منفصلة
استخدم view.bounds عند طلب منطقة الرسم المحلية كاملةً. تصف حدود المنظور نظام إحداثياته الخاص، بينما يصف إطاره موضعه وحجمه في نظام إحداثيات المنظور الفائق. لذلك قد يؤدي تمرير الإطار إلى طريقة تتوقع منطقة محلية إلى إزاحة غير مقصودة. توثيق Apple للحدود.
في مثال توضيحي لمواصفات الحجم، قد تُعرض صورة نقطية مصدرية بحجم 192 × 128 على هيئة صورة منطقية بحجم 96 × 64. حافظ على القيمتين كلتيهما: فالأولى تصف العينات، والثانية تصف حجم العرض. يقبل مُهيّئ NSImage حجمًا صريحًا؛ أما تمرير صفر فيجعله يعتمد أبعاد بكسل الصورة المصدرية. توثيق Apple لمُهيّئ الصور.
عند تشخيص هندسة التخزين الداعمة لمنظور موجود، استخدم convertToBacking(_:)، الذي يحوّل مستطيله المحلي إلى إحداثيات مخزن الدعم. أما بالنسبة إلى الصورة النقطية المعادة فعليًا، فافحص pixelsWide وpixelsHigh أو أبعاد CGImage الخاصة بها. توثّق Apple أن cgImage في NSBitmapImageRep له أبعاد البكسل نفسها التي للتمثيل. توثيق التحويل إلى إحداثيات الدعم، توثيق CGImage للصورة النقطية.
لا تستبدل تلك الأبعاد المعادة بمعامل 2× مفترض. سجّل معًا المستطيل المحلي المطلوب، وعرض البكسل وارتفاعه في الناتج، والحجم المنطقي المقصود. يمنحك ذلك مقارنة ملموسة عندما تبدو النتيجة مكبّرة أو مقصوصة أو مزاحة.
أنشئ مثالًا كاملًا للتحويل الاصطناعي إلى صورة نقطية
ينشئ برنامج Objective-C التالي صورة اختبار غير متناظرة، ويسندها إلى منظور صور في الذاكرة، ويحوّل المنظور إلى صورة نقطية، ثم يوحّد الناتج في صورة نقطية ضمن Core Graphics. لا ينشئ البرنامج أي ملفات. احفظه باسم RasterDemo.m على جهاز Mac مثبتة عليه أدوات المطور لسطر الأوامر.
clang -fobjc-arc RasterDemo.m -framework Cocoa -o RasterDemo
./RasterDemo
يستخدم سياق التوحيد مكوّنات من ثمانية بتات، وألفا مضروبًا مسبقًا في الموضع الأعلى أهمية، وتخزينًا بترتيب 32 بت ذي نهاية صغرى. تنتج هذه الخيارات مجتمعةً ترتيب بايتات BGRA: يأتي مكوّن الأزرق الأقل أهمية أولًا في الذاكرة. ويتبع ذلك تعريفات Apple لـ premultipliedFirst وbyteOrder32Little.
#import <Cocoa/Cocoa.h>
static CGContextRef NewBitmap(size_t w, size_t h) {
// Illustrative allocation budget, not an API dimension limit.
if (w == 0 || h == 0 || w > 4096 || h > 4096) return NULL;
size_t rowBytes = (w * 4 + 63) & ~(size_t)63;
CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
if (!space) return NULL;
CGContextRef c = CGBitmapContextCreate(
NULL, w, h, 8, rowBytes, space,
kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
CGColorSpaceRelease(space);
return c;
}
static CGImageRef NewTestImage(void) {
CGContextRef c = NewBitmap(192, 128);
if (!c) return NULL;
CGContextSetRGBFillColor(c, 1, 1, 1, 1);
CGContextFillRect(c, CGRectMake(0, 0, 192, 128));
CGContextSetRGBFillColor(c, 0, 0, 0, 1);
for (size_t x = 0; x < 192; x += 16) {
CGContextFillRect(c, CGRectMake(x, 0, 1, 128));
}
CGContextSetRGBFillColor(c, 0, 0.7, 0.8, 1);
CGContextFillRect(c, CGRectMake(8, 92, 24, 20));
CGImageRef image = CGBitmapContextCreateImage(c);
CGContextRelease(c);
return image;
}
static CGImageRef NewNormalizedImage(CGImageRef input) {
size_t w = CGImageGetWidth(input);
size_t h = CGImageGetHeight(input);
CGContextRef c = NewBitmap(w, h);
if (!c) return NULL;
CGContextClearRect(c, CGRectMake(0, 0, w, h));
CGContextSetBlendMode(c, kCGBlendModeCopy);
CGContextSetInterpolationQuality(c, kCGInterpolationNone);
CGContextDrawImage(c, CGRectMake(0, 0, w, h), input);
NSLog(@"Normalized: %zu x %zu pixels; stride %zu bytes",
w, h, CGBitmapContextGetBytesPerRow(c));
CGImageRef result = CGBitmapContextCreateImage(c);
CGContextRelease(c);
return result;
}
int main(void) {
@autoreleasepool {
[NSApplication sharedApplication];
CGImageRef source = NewTestImage();
if (!source) { NSLog(@"Source allocation failed"); return 1; }
NSImage *image = [[NSImage alloc] initWithCGImage:source
size:NSMakeSize(96, 64)];
CGImageRelease(source);
NSImageView *view = [[NSImageView alloc]
initWithFrame:NSMakeRect(0, 0, 96, 64)];
view.image = image;
view.imageScaling = NSImageScaleNone;
view.imageAlignment = NSImageAlignCenter;
view.imageFrameStyle = NSImageFrameNone;
[view layoutSubtreeIfNeeded];
NSRect region = view.bounds;
NSBitmapImageRep *rep =
[view bitmapImageRepForCachingDisplayInRect:region];
if (!rep) { NSLog(@"View bitmap allocation failed"); return 1; }
[view cacheDisplayInRect:region toBitmapImageRep:rep];
CGImageRef cached = rep.CGImage;
if (!cached) { NSLog(@"View bitmap has no CGImage"); return 1; }
CGImageRef raster = NewNormalizedImage(cached);
if (!raster) { NSLog(@"Normalization failed"); return 1; }
NSImage *result = [[NSImage alloc] initWithCGImage:raster
size:region.size];
NSLog(@"Result logical size: %@", NSStringFromSize(result.size));
CGImageRelease(raster);
}
return 0;
}
يتعمّد البرنامج الإبلاغ عن أبعاد الناتج الفعلية بدلًا من افتراض أن ذاكرة التخزين المؤقت للمنظور تطابق صورة المصدر ذات 192 × 128 بكسل. وتتيح العلامة السماوية تمييز تغييرات الاتجاه عن خطوط الاختبار الرأسية المكررة. إذا كان شرطك هو دقة المصدر تمامًا، فاستخدم CGImage المعاد من NewTestImage() مباشرةً بدلًا من المرور عبر المنظور.
يهيئ المسح الصريح بكسلات الوجهة لتكون شفافة قبل التوحيد. ويرسم استدعاء الرسم لصورة واحدة المستطيل الوجهة بالكامل؛ ولا يكرّر الصورة. توثّق Apple أن عملية الرسم المقابلة في Core Graphics تحجّم الصورة لتلائم مستطيل الوجهة وترسم نسخة واحدة ما لم يُفعّل التبليط. توثيق Apple لرسم الصور.
فُحص هذا البرنامج، لكن تعذّر تجميعه في بيئة أبحاث Linux، التي تفتقر إلى AppKit ومترجم C. وقد جرى التحقق من حسابات خطوة الصف المستخدمة أدناه باستخدام عملية حسابية صغيرة وحتمية بلغة Python.
شخّص البكسلات المزاحة أو المكررة بترتيب ثابت
التسلسل التالي أداة تشخيصية مستمدة من مواصفات الهندسة والذاكرة أعلاه. تشير الأعراض إلى المواضع التي ينبغي فحصها أولًا؛ لكنها لا تثبت سببًا واحدًا بعينه.
- افحص المستطيل المطلوب. بالنسبة إلى منظور الصورة كاملًا، قارن طلب التخزين المؤقت مع
bounds. عند تصدير منطقة فرعية، احتفظ بأصلها المحلي ضمن مواصفات الإدخال. - افحص الأبعاد قبل البايتات الخام. قارن عرض الصورة المخزنة مؤقتًا وارتفاعها بالأبعاد المقدمة إلى كل مستهلك لاحق. فاستبدال عرض النقاط بعرض البكسلات يغيّر تفسير المخزن المؤقت.
- افحص خطوة الصف. إذا بدأ كل صف على مسافة أبعد تدريجيًا من موضعه المتوقع، فتحقق من المسافة بين بدايات الصفوف قبل تعديل التحويلات.
- تحقق من الاتجاه باستخدام العلامة غير المتناظرة. يستدعي الانعكاس العمودي فحص اصطلاحات الإحداثيات. أما الإزاحة الثابتة فتستدعي فحص الأصول والترجمات.
- تحقق مما إذا كان التكرار مقصودًا. افحص أي استدعاء رسم يفعّل التبليط. لن يصحح تغيير خطوة الصف رسمًا مبلطًا صراحةً.
- افحص تحجيم العرض أخيرًا. إذا كانت الصورة النقطية المخزنة صحيحة لكن النتيجة المعروضة مختلفة، فقارن الحجم المنطقي لصورة الإخراج وإعدادات التحجيم في منظور الصور المستقبِل.
بالنسبة إلى بكسلات من أربعة بايتات، يحتوي المخزن التوضيحي بحجم 192 × 128 على 768 بايتًا لكل صف، ويتطلب 98,304 بايتًا. لكن المخزن بحجم 191 × 128 مع محاذاة الصفوف البالغة 64 بايتًا في المثال، له أيضًا خطوة من 768 بايتًا: يتضمن كل صف أربعة بايتات حشو. إن التقدم بمقدار 191 × 4، أو 764، سيبدأ كل صف لاحق قبل موضعه بأربعة بايتات. تتيح Apple الوصول إلى خطوة الصف الفعلية عبر خاصية السياق bytesPerRow.
استخدم حساب العنوان base + y * bytesPerRow + x * 4 فقط مع التنسيق الموحّد ذي الأربعة بايتات المحدد هنا. للصورة النقطية الاعتباطية من AppKit مواصفات تنسيق خاصة بها؛ فلا تستنتج تخطيط مكوّناتها من مظهرها وحده.
وبالمثل، طبّق تغييرات الإحداثيات عمدًا. توفر Core Graphics عمليتين منفصلتين لتغيير أصل فضاء المستخدم ومقياسه. إذا أضفت قلبًا رأسيًا، فوثّق المرحلة المسؤولة عن هذا التحويل وتحقق منه باستخدام العلامة قبل إضافة تحويل آخر. توثيق الترجمة، توثيق القياس.
حافظ على الملكية عند إرجاع صورة أو رفعها إلى Metal
يُبقي المثال تمثيل AppKit حيًا أثناء رسم CGImage الخاص به. ثم يعيد لقطة أُنشئت من سياق التوحيد. توثّق Apple أن makeImage() ينشئ نسخة، وقد يستخدم داخليًا النسخ عند الكتابة، وأن الرسم اللاحق في السياق لا يغيّر الصورة المعادة. يتيح ذلك تحرير السياق بعد إنشاء اللقطة. توثيق Apple للقطات الصور النقطية.
لمؤشر الصورة النقطية الخام مدة حياة مختلفة. توفر Apple إمكانية الوصول إلى بيانات الصورة النقطية المملوكة للسياق حتى عندما أُنشئ السياق مع وسيط بيانات فارغ. تعامل مع ذلك المؤشر على أنه تابع للسياق ما دام حيًا؛ ولأي مستهلك لاحق، انسخ البايتات إلى مساحة تخزين تملكها أو أبقِ السياق حيًا حتى يكتمل الاستهلاك. توثيق Apple لبيانات الصور النقطية.
لرفع الصورة إلى Metal، نفّذ الخطوات التالية بينما يظل سياق التوحيد حيًا:
- أنشئ نسيجًا ثنائي الأبعاد بعرض البكسل وارتفاعه في السياق. استخدم
bgra8Unormلتخطيط المكوّنات BGRA الموضح؛ وتعرّفه Apple بأنه أربعة مكوّنات غير موقّعة ومطبّعة، كل منها بثمانية بتات، بترتيب BGRA. - عند الرفع من وحدة المعالجة المركزية، أبقِ على نمط التخزين الافتراضي للنظام بدلًا من فرض نمط مشاركة خاص بالأجهزة. توثّق Apple أن الإعداد الافتراضي هو المشترك على أجهزة Apple silicon، والمدار للأنسجة على أجهزة Mac المستندة إلى Intel.
- استدعِ
replace(region:mipmapLevel:withBytes:bytesPerRow:)لمنطقة النسيج كاملةً، ومستوى mipmap0، باستخدام مؤشر بيانات السياق وخطوة الصف الفعلية. - أكمل الرفع قبل تحرير السياق. ولإعادة استخدام النسيج، تأكد أيضًا من انتهاء عمل GPU السابق الذي يستخدمه قبل أن تستبدل CPU بياناته.
تتبع خيارات التنسيق والتخزين وثائق Apple الخاصة بـ bgra8Unorm وإرشادات تخزين الموارد. وتذكر Apple أن طريقة الاستبدال تنسخ البكسلات المقدمة فورًا، لكنها لا تزامن مع عمليات GPU. أما النسيج الخاص، فالمسار الموثّق هو استخدام نسيج مؤقت غير خاص ثم تنفيذ عملية نسخ كتلي. توثيق Apple لاستبدال النسيج.
يحافظ رفع النسيج على البايتات المقدمة؛ لكن تطابق تخطيط المكوّنات وحده لا يحدد بقية مسار التصيير. أدرج قرارات ألفا المضروب مسبقًا وsRGB في المثال ضمن تصميم المستهلك. التوحيد هو إعادة رسم بتنسيق مختار، لذا لا ينبغي وصفه بأنه ضمان لتحويل مطابق للبايتات من كل تمثيل AppKit ممكن.
في هذه المهمة، يتكون التسليم الموثوق من الصورة النقطية وأبعاد البكسل وحجم العرض المنطقي وخطوة الصف وتنسيق المكوّنات واصطلاح الاتجاه والمالك. أبقِ هذه القيم معًا، واختبر برسم اصطناعي غير متناظر، وصحح أول مرحلة تبدأ فيها القيم بالاختلاف. يجعل ذلك مشكلة الإزاحة أو التكرار قابلة للتتبع بوصفها مشكلة في المواصفات.
المصادر
cacheDisplay(in:to:) — Apple
imageScaling — Apple
bounds — Apple
init(cgImage:size:) — Apple
convertToBacking(_:) — Apple
cgImage — Apple
byteOrder32Little — Apple
draw(_:in:byTiling:) — Apple
bytesPerRow — Apple
translateBy(x:y:) — Apple
scaleBy(x:y:) — Apple
makeImage() — Apple
data — Apple
MTLPixelFormat.bgra8Unorm — Apple
Setting resource storage modes — Apple