xref: /haiku/src/libs/print/libprint/GraphicsDriver.cpp (revision 08d759feae5967ad75d0f0d4ee33c21c72ae6db8)
1 /*
2  * GraphicsDriver.cpp
3  * Copyright 1999-2000 Y.Takagi. All Rights Reserved.
4  */
5 
6 #include <algorithm>
7 #include <cstdio>
8 #include <cstdarg>
9 
10 #include <Alert.h>
11 #include <Bitmap.h>
12 #include <Message.h>
13 #include <PrintJob.h>
14 #include <Region.h>
15 #include <TextControl.h>
16 #include <TextControl.h>
17 #include <StopWatch.h>
18 #include <View.h>
19 #include <Directory.h>
20 #include <File.h>
21 
22 #include "GraphicsDriver.h"
23 #include "PrintProcess.h"
24 #include "JobData.h"
25 #include "PrinterData.h"
26 #include "PrinterCap.h"
27 #include "Preview.h"
28 #include "Transport.h"
29 #include "ValidRect.h"
30 #include "DbgMsg.h"
31 
32 
33 using namespace std;
34 
35 
36 // Measure printJob() time. Either true or false.
37 #define MEASURE_PRINT_JOB_TIME false
38 
39 enum {
40 	kMaxMemorySize = (4 *1024 *1024)
41 };
42 
43 GraphicsDriver::GraphicsDriver(BMessage *msg, PrinterData *printer_data, const PrinterCap *printer_cap)
44 	: fMsg(msg)
45 	, fPrinterData(printer_data)
46 	, fPrinterCap(printer_cap)
47 {
48 	fView          = NULL;
49 	fBitmap        = NULL;
50 	fTransport     = NULL;
51 	fOrgJobData    = NULL;
52 	fRealJobData   = NULL;
53 	fSpoolMetaData = NULL;
54 }
55 
56 GraphicsDriver::~GraphicsDriver()
57 {
58 }
59 
60 
61 static BRect RotateRect(BRect rect)
62 {
63 	BRect rotated(rect.top, rect.left, rect.bottom, rect.right);
64 	return rotated;
65 }
66 
67 bool
68 GraphicsDriver::setupData(BFile *spoolFile)
69 {
70 	if (fOrgJobData != NULL) {
71 		// already initialized
72 		return true;
73 	}
74 
75 #ifndef B_BEOS_VERSION_DANO
76 	print_file_header pfh;
77 #else
78 	BPrintJob::print_file_header pfh;
79 #endif
80 	spoolFile->Seek(0, SEEK_SET);
81 	spoolFile->Read(&pfh, sizeof(pfh));
82 
83 	DBGMSG(("print_file_header::version = 0x%x\n",  pfh.version));
84 	DBGMSG(("print_file_header::page_count = %d\n", pfh.page_count));
85 	DBGMSG(("print_file_header::first_page = 0x%x\n", (int)pfh.first_page));
86 
87 	if (pfh.page_count <= 0) {
88 		// nothing to print
89 		return false;
90 	}
91 
92 	fPageCount = (uint32) pfh.page_count;
93 	BMessage *msg = new BMessage();
94 	msg->Unflatten(spoolFile);
95 	fOrgJobData = new JobData(msg, fPrinterCap, JobData::kJobSettings);
96 	DUMP_BMESSAGE(msg);
97 	delete msg;
98 
99 	fRealJobData = new JobData(*fOrgJobData);
100 
101 	switch (fOrgJobData->getNup()) {
102 	case 2:
103 	case 8:
104 	case 32:
105 	case 128:
106 		fRealJobData->setPrintableRect(RotateRect(fOrgJobData->getPrintableRect()));
107 		fRealJobData->setScaledPrintableRect(RotateRect(fOrgJobData->getScaledPrintableRect()));
108 		fRealJobData->setPhysicalRect(RotateRect(fOrgJobData->getPhysicalRect()));
109 		fRealJobData->setScaledPhysicalRect(RotateRect(fOrgJobData->getScaledPhysicalRect()));
110 		if (JobData::kPortrait == fOrgJobData->getOrientation())
111 			fRealJobData->setOrientation(JobData::kLandscape);
112 		else
113 			fRealJobData->setOrientation(JobData::kPortrait);
114 		break;
115 	}
116 
117 	if (fOrgJobData->getCollate() && fPageCount > 1) {
118 		fRealJobData->setCopies(1);
119 		fInternalCopies = fOrgJobData->getCopies();
120 	} else {
121 		fInternalCopies = 1;
122 	}
123 
124 	fSpoolMetaData = new SpoolMetaData(spoolFile);
125 	return true;
126 }
127 
128 void
129 GraphicsDriver::cleanupData()
130 {
131 	delete fRealJobData;
132 	delete fOrgJobData;
133 	delete fSpoolMetaData;
134 	fRealJobData   = NULL;
135 	fOrgJobData    = NULL;
136 	fSpoolMetaData = NULL;
137 }
138 
139 void
140 GraphicsDriver::setupBitmap()
141 {
142 	fPixelDepth = color_space2pixel_depth(fOrgJobData->getSurfaceType());
143 
144 	fPageWidth  = (fRealJobData->getPhysicalRect().IntegerWidth()  * fOrgJobData->getXres() + 71) / 72;
145 	fPageHeight = (fRealJobData->getPhysicalRect().IntegerHeight() * fOrgJobData->getYres() + 71) / 72;
146 
147 	int widthByte = (fPageWidth * fPixelDepth + 7) / 8;
148 	int size = widthByte * fPageHeight;
149 #ifdef USE_PREVIEW_FOR_DEBUG
150 	size = 0;
151 #endif
152 
153 	if (size < kMaxMemorySize) {
154 		fBandCount  = 0;
155 		fBandWidth  = fPageWidth;
156 		fBandHeight = fPageHeight;
157 	} else {
158 		fBandCount  = (size + kMaxMemorySize - 1) / kMaxMemorySize;
159 		if ((JobData::kLandscape == fRealJobData->getOrientation()) && (fFlags & kGDFRotateBandBitmap)) {
160 			fBandWidth  = (fPageWidth + fBandCount - 1) / fBandCount;
161 			fBandHeight = fPageHeight;
162 		} else {
163 			fBandWidth  = fPageWidth;
164 			fBandHeight = (fPageHeight + fBandCount - 1) / fBandCount;
165 		}
166 	}
167 
168 	DBGMSG(("****************\n"));
169 	DBGMSG(("page_width  = %d\n", fPageWidth));
170 	DBGMSG(("page_height = %d\n", fPageHeight));
171 	DBGMSG(("band_count  = %d\n", fBandCount));
172 	DBGMSG(("band_height = %d\n", fBandHeight));
173 	DBGMSG(("****************\n"));
174 
175 	BRect rect;
176 	rect.Set(0, 0, fBandWidth - 1, fBandHeight - 1);
177 	fBitmap = new BBitmap(rect, fOrgJobData->getSurfaceType(), true);
178 	fView   = new BView(rect, "", B_FOLLOW_ALL, B_WILL_DRAW);
179 	fBitmap->AddChild(fView);
180 }
181 
182 void
183 GraphicsDriver::cleanupBitmap()
184 {
185 	delete fBitmap;
186 	fBitmap = NULL;
187 	fView   = NULL;
188 }
189 
190 BPoint
191 GraphicsDriver::getScale(int32 nup, BRect physicalRect, float scaling)
192 {
193 	float width;
194 	float height;
195 	BPoint scale;
196 
197 	scale.x = scale.y = 1.0f;
198 
199 	switch (nup) {
200 	case 1:
201 		scale.x = scale.y = 1.0f;
202 		break;
203 	case 2:	/* 1x2 or 2x1 */
204 		width  = physicalRect.Width();
205 		height = physicalRect.Height();
206 		if (width < height) {	// portrait
207 			scale.x = height / 2.0f / width;
208 			scale.y = width / height;
209 		} else {	// landscape
210 			scale.x = height / width;
211 			scale.y = width / 2.0f / height;
212 		}
213 		break;
214 	case 8:	/* 2x4 or 4x2 */
215 		width  = physicalRect.Width();
216 		height = physicalRect.Height();
217 		if (width < height) {
218 			scale.x = height / 4.0f / width;
219 			scale.y = width / height / 2.0f;
220 		} else {
221 			scale.x = height / width / 2.0f;
222 			scale.y = width / 4.0f / height;
223 		}
224 		break;
225 	case 32:	/* 4x8 or 8x4 */
226 		width  = physicalRect.Width();
227 		height = physicalRect.Height();
228 		if (width < height) {
229 			scale.x = height / 8.0f / width;
230 			scale.y = width / height / 4.0f;
231 		} else {
232 			scale.x = height / width / 4.0f;
233 			scale.y = width / 8.0f / height;
234 		}
235 		break;
236 	case 4:		/* 2x2 */
237 		scale.x = scale.y = 1.0f / 2.0f;
238 		break;
239 	case 9:		/* 3x3 */
240 		scale.x = scale.y = 1.0f / 3.0f;
241 		break;
242 	case 16:	/* 4x4 */
243 		scale.x = scale.y = 1.0f / 4.0f;
244 		break;
245 	case 25:	/* 5x5 */
246 		scale.x = scale.y = 1.0f / 5.0f;
247 		break;
248 	case 36:	/* 6x6 */
249 		scale.x = scale.y = 1.0f / 6.0f;
250 		break;
251 	case 49:	/* 7x7 */
252 		scale.x = scale.y = 1.0f / 7.0f;
253 		break;
254 	case 64:	/* 8x8 */
255 		scale.x = scale.y = 1.0f / 8.0f;
256 		break;
257 	case 81:	/* 9x9 */
258 		scale.x = scale.y = 1.0f / 9.0f;
259 		break;
260 	case 100:	/* 10x10 */
261 		scale.x = scale.y = 1.0f / 10.0f;
262 		break;
263 	case 121:	/* 11x11 */
264 		scale.x = scale.y = 1.0f / 11.0f;
265 		break;
266 	}
267 
268 	scale.x = scale.x * scaling / 100.0;
269 	scale.y = scale.y * scaling / 100.0;
270 
271 	return scale;
272 }
273 
274 BPoint
275 GraphicsDriver::getOffset(int32 nup, int index, JobData::Orientation orientation,
276 	const BPoint *scale, BRect scaledPhysicalRect, BRect scaledPrintableRect,
277 	BRect physicalRect)
278 {
279 	BPoint offset;
280 	offset.x = 0;
281 	offset.y = 0;
282 
283 	float width  = scaledPhysicalRect.Width();
284 	float height = scaledPhysicalRect.Height();
285 
286 	switch (nup) {
287 	case 1:
288 		break;
289 	case 2:
290 		if (index == 1) {
291 			if (JobData::kPortrait == orientation) {
292 				offset.x = width;
293 			} else {
294 				offset.y = height;
295 			}
296 		}
297 		break;
298 	case 8:
299 		if (JobData::kPortrait == orientation) {
300 			offset.x = width  * (index / 2);
301 			offset.y = height * (index % 2);
302 		} else {
303 			offset.x = width  * (index % 2);
304 			offset.y = height * (index / 2);
305 		}
306 		break;
307 	case 32:
308 		if (JobData::kPortrait == orientation) {
309 			offset.x = width  * (index / 4);
310 			offset.y = height * (index % 4);
311 		} else {
312 			offset.x = width  * (index % 4);
313 			offset.y = height * (index / 4);
314 		}
315 		break;
316 	case 4:
317 		offset.x = width  * (index / 2);
318 		offset.y = height * (index % 2);
319 		break;
320 	case 9:
321 		offset.x = width  * (index / 3);
322 		offset.y = height * (index % 3);
323 		break;
324 	case 16:
325 		offset.x = width  * (index / 4);
326 		offset.y = height * (index % 4);
327 		break;
328 	case 25:
329 		offset.x = width  * (index / 5);
330 		offset.y = height * (index % 5);
331 		break;
332 	case 36:
333 		offset.x = width  * (index / 6);
334 		offset.y = height * (index % 6);
335 		break;
336 	case 49:
337 		offset.x = width  * (index / 7);
338 		offset.y = height * (index % 7);
339 		break;
340 	case 64:
341 		offset.x = width  * (index / 8);
342 		offset.y = height * (index % 8);
343 		break;
344 	case 81:
345 		offset.x = width  * (index / 9);
346 		offset.y = height * (index % 9);
347 		break;
348 	case 100:
349 		offset.x = width  * (index / 10);
350 		offset.y = height * (index % 10);
351 		break;
352 	case 121:
353 		offset.x = width  * (index / 11);
354 		offset.y = height * (index % 11);
355 		break;
356 	}
357 
358 	// adjust margin
359 	offset.x += scaledPrintableRect.left - physicalRect.left;
360 	offset.y += scaledPrintableRect.top - physicalRect.top;
361 
362 	float real_scale = min(scale->x, scale->y);
363 	if (real_scale != scale->x)
364 		offset.x *= scale->x / real_scale;
365 	else
366 		offset.y *= scale->y / real_scale;
367 
368 	return offset;
369 }
370 
371 // print the specified pages on a physical page
372 bool
373 GraphicsDriver::printPage(PageDataList *pages)
374 {
375 	BPoint offset;
376 	offset.x = 0.0f;
377 	offset.y = 0.0f;
378 
379 	if (pages == NULL) {
380 		return true;
381 	}
382 
383 	do {
384 		// clear the physical page
385 		fView->SetScale(1.0);
386 		fView->SetHighColor(255, 255, 255);
387 		fView->ConstrainClippingRegion(NULL);
388 		fView->FillRect(fView->Bounds());
389 
390 		BPoint scale = getScale(fOrgJobData->getNup(), fOrgJobData->getPhysicalRect(), fOrgJobData->getScaling());
391 		float real_scale = min(scale.x, scale.y) * fOrgJobData->getXres() / 72.0f;
392 		fView->SetScale(real_scale);
393 		float x = offset.x / real_scale;
394 		float y = offset.y / real_scale;
395 		int page_index = 0;
396 
397 		for (PageDataList::iterator it = pages->begin(); it != pages->end(); it++) {
398 			BPoint left_top(getOffset(fOrgJobData->getNup(), page_index++, fOrgJobData->getOrientation(), &scale, fOrgJobData->getScaledPhysicalRect(), fOrgJobData->getScaledPrintableRect(), fOrgJobData->getPhysicalRect()));
399 			left_top.x -= x;
400 			left_top.y -= y;
401 			BRect clip(fOrgJobData->getScaledPrintableRect());
402 			clip.OffsetTo(left_top);
403 			BRegion *region = new BRegion();
404 			region->Set(clip);
405 			fView->ConstrainClippingRegion(region);
406 			delete region;
407 			if ((*it)->startEnum()) {
408 				bool more;
409 				do {
410 					PictureData	*picture_data;
411 					more = (*it)->enumObject(&picture_data);
412 					BPoint real_offset = left_top + picture_data->point;
413 					fView->DrawPicture(picture_data->picture, real_offset);
414 					fView->Sync();
415 					delete picture_data;
416 				} while (more);
417 			}
418 		}
419 		if (!nextBand(fBitmap, &offset)) {
420 			return false;
421 		}
422 	} while (offset.x >= 0.0f && offset.y >= 0.0f);
423 
424 	return true;
425 }
426 
427 bool
428 GraphicsDriver::collectPages(SpoolData *spool_data, PageDataList *pages)
429 {
430 	// collect the pages to be printed on the physical page
431 	PageData *page_data;
432 	int nup = fOrgJobData->getNup();
433 	bool more;
434 	do {
435 		more = spool_data->enumObject(&page_data);
436 		if (pages != NULL) {
437 			pages->push_back(page_data);
438 		}
439 	} while (more && --nup);
440 
441 	return more;
442 }
443 
444 bool
445 GraphicsDriver::skipPages(SpoolData *spool_data)
446 {
447 	return collectPages(spool_data, NULL);
448 }
449 
450 bool
451 GraphicsDriver::printDocument(SpoolData *spool_data)
452 {
453 	bool more;
454 	bool success;
455 	int page_index;
456 	int copy;
457 	int copies;
458 
459 	more = true;
460 	success = true;
461 	page_index = 0;
462 
463 	if (fPrinterCap->isSupport(PrinterCap::kCopyCommand)) {
464 		// let the printer perform the copy operation
465 		copies = 1;
466 	} else {
467 		// send the page multiple times to the printer
468 		copies = fRealJobData->getCopies();
469 	}
470 	fStatusWindow -> SetPageCopies(copies);						// inform fStatusWindow about number of copies
471 
472 	// printing of even/odd numbered pages only is valid in simplex mode
473 	bool simplex = fRealJobData->getPrintStyle() == JobData::kSimplex;
474 
475 	if (spool_data->startEnum()) {
476 		do {
477 			DBGMSG(("page index = %d\n", page_index));
478 
479 			if (simplex &&
480 				fRealJobData->getPageSelection() == JobData::kEvenNumberedPages) {
481 				// skip odd numbered page
482 				more = skipPages(spool_data);
483 			}
484 
485 			if (!more) {
486 				// end reached
487 				break;
488 			}
489 
490 			PageDataList pages;
491 			more = collectPages(spool_data, &pages);
492 
493 			if (more && simplex &&
494 				fRealJobData->getPageSelection() == JobData::kOddNumberedPages) {
495 				// skip even numbered page
496 				more = skipPages(spool_data);
497 			}
498 
499 			// print each physical page "copies" of times
500 			for (copy = 0; success && copy < copies; copy ++) {
501 
502 				// Update the status / cancel job
503 				if (fStatusWindow->UpdateStatusBar(page_index, copy))
504 					return false;
505 
506 				success = startPage(page_index);
507 				if (!success)
508 					break;
509 
510 				// print the pages on the physical page
511 				fView->Window()->Lock();
512 				success = printPage(&pages);
513 				fView->Window()->Unlock();
514 
515 				if (success) {
516 					success = endPage(page_index);
517 				}
518 			}
519 
520 			page_index++;
521 		} while (success && more);
522 	}
523 
524 #ifndef USE_PREVIEW_FOR_DEBUG
525 	if (success
526 		&& fPrinterCap->isSupport(PrinterCap::kPrintStyle)
527 		&& (fOrgJobData->getPrintStyle() != JobData::kSimplex)
528 		&& (((page_index + fOrgJobData->getNup() - 1) / fOrgJobData->getNup()) % 2))
529 	{
530 		// append an empty page on the back side of the page in duplex or booklet mode
531 		success =
532 			startPage(page_index) &&
533 			printPage(NULL) &&
534 			endPage(page_index);
535 	}
536 #endif
537 
538 	return success;
539 }
540 
541 const JobData*
542 GraphicsDriver::getJobData(BFile *spoolFile)
543 {
544 	setupData(spoolFile);
545 	return fOrgJobData;
546 }
547 
548 bool
549 GraphicsDriver::printJob(BFile *spoolFile)
550 {
551 	bool success = true;
552 	if (!setupData(spoolFile)) {
553 		// silently exit if there is nothing to print
554 		return true;
555 	}
556 
557 	fTransport = new Transport(fPrinterData);
558 
559 	if (fTransport->check_abort()) {
560 		success = false;
561 	} else if (!fTransport->is_print_to_file_canceled()) {
562 		BStopWatch stopWatch("printJob", !MEASURE_PRINT_JOB_TIME);
563 		setupBitmap();
564 		SpoolData spool_data(spoolFile, fPageCount, fOrgJobData->getNup(), fOrgJobData->getReverse());
565 		success = startDoc();
566 		if (success) {
567 			fStatusWindow = new StatusWindow(
568 				fRealJobData->getPageSelection() == JobData::kOddNumberedPages,
569 				fRealJobData->getPageSelection() == JobData::kEvenNumberedPages,
570 				fRealJobData->getFirstPage(),
571 				fPageCount,
572 				fInternalCopies,fRealJobData->getNup());
573 
574 			while (fInternalCopies--) {
575 				success = printDocument(&spool_data);
576 				if (success == false) {
577 					break;
578 				}
579 			}
580 			endDoc(success);
581 
582 			fStatusWindow->Lock();
583 			fStatusWindow->Quit();
584 		}
585 		cleanupBitmap();
586 		cleanupData();
587 	}
588 
589 	if (success == false) {
590 		BAlert *alert;
591 		if (fTransport->check_abort()) {
592 			alert = new BAlert("", fTransport->last_error().c_str(), "OK");
593 		} else {
594 			alert = new BAlert("", "Printer not responding.", "OK");
595 		}
596 		alert->Go();
597 	}
598 
599 	delete fTransport;
600 	fTransport = NULL;
601 
602 	return success;
603 }
604 
605 BMessage *
606 GraphicsDriver::takeJob(BFile* spoolFile, uint32 flags)
607 {
608 	fFlags = flags;
609 	BMessage *msg;
610 	if (printJob(spoolFile)) {
611 		msg = new BMessage('okok');
612 	} else {
613 		msg = new BMessage('baad');
614 	}
615 	return msg;
616 }
617 
618 bool
619 GraphicsDriver::startDoc()
620 {
621 	return true;
622 }
623 
624 bool
625 GraphicsDriver::startPage(int)
626 {
627 	return true;
628 }
629 
630 bool
631 GraphicsDriver::nextBand(BBitmap *, BPoint *)
632 {
633 	return true;
634 }
635 
636 bool
637 GraphicsDriver::endPage(int)
638 {
639 	return true;
640 }
641 
642 bool
643 GraphicsDriver::endDoc(bool)
644 {
645 	return true;
646 }
647 
648 void
649 GraphicsDriver::writeSpoolData(const void *buffer, size_t size) throw(TransportException)
650 {
651 	if (fTransport) {
652 		fTransport->write(buffer, size);
653 	}
654 }
655 
656 void
657 GraphicsDriver::writeSpoolString(const char *format, ...) throw(TransportException)
658 {
659 	if (fTransport) {
660 		char buffer[256];
661 		va_list	ap;
662 		va_start(ap, format);
663 		vsprintf(buffer, format, ap);
664 		fTransport->write(buffer, strlen(buffer));
665 		va_end(ap);
666 	}
667 }
668 
669 void
670 GraphicsDriver::writeSpoolChar(char c) throw(TransportException)
671 {
672 	if (fTransport) {
673 		fTransport->write(&c, 1);
674 	}
675 }
676 
677 
678 void
679 GraphicsDriver::rgb32_to_rgb24(void* src, void* dst, int width) {
680 	uint8* d = (uint8*)dst;
681 	rgb_color* s = (rgb_color*)src;
682 	for (int i = width; i > 0; i --) {
683 		*d ++ = s->red;
684 		*d ++ = s->green;
685 		*d ++ = s->blue;
686 		s++;
687 	}
688 }
689 
690 void
691 GraphicsDriver::cmap8_to_rgb24(void* src, void* dst, int width) {
692 	uint8* d = (uint8*)dst;
693 	uint8* s = (uint8*)src;
694 	const color_map* cmap = system_colors();
695 	for (int i = width; i > 0; i --) {
696 		const rgb_color* rgb = &cmap->color_list[*s];
697 		*d ++ = rgb->red;
698 		*d ++ = rgb->green;
699 		*d ++ = rgb->blue;
700 		s ++;
701 	}
702 }
703 
704 void
705 GraphicsDriver::convert_to_rgb24(void* src, void* dst, int width, color_space cs) {
706 	if (cs == B_RGB32) rgb32_to_rgb24(src, dst, width);
707 	else if (cs == B_CMAP8) cmap8_to_rgb24(src, dst, width);
708 	else {
709 		DBGMSG(("color_space %d not supported", cs));
710 	}
711 }
712 
713 uint8
714 GraphicsDriver::gray(uint8 r, uint8 g, uint8 b) {
715 	if (r == g && g == b) {
716 		return r;
717 	} else {
718 		return (r * 3 + g * 6 + b) / 10;
719 	}
720 }
721 
722 
723 void
724 GraphicsDriver::rgb32_to_gray(void* src, void* dst, int width) {
725 	uint8* d = (uint8*)dst;
726 	rgb_color* s = (rgb_color*)src;
727 	for (int i = width; i > 0; i --) {
728 		*d ++ = gray(s->red, s->green, s->blue);
729 		s++;
730 	}
731 }
732 
733 void
734 GraphicsDriver::cmap8_to_gray(void* src, void* dst, int width) {
735 	uint8* d = (uint8*)dst;
736 	uint8* s = (uint8*)src;
737 	const color_map* cmap = system_colors();
738 	for (int i = width; i > 0; i --) {
739 		const rgb_color* rgb = &cmap->color_list[*s];
740 		*d ++ = gray(rgb->red, rgb->green, rgb->blue);
741 		s ++;
742 	}
743 }
744 
745 void
746 GraphicsDriver::convert_to_gray(void* src, void* dst, int width, color_space cs) {
747 	if (cs == B_RGB32) rgb32_to_gray(src, dst, width);
748 	else if (cs == B_CMAP8) cmap8_to_gray(src, dst, width);
749 	else {
750 		DBGMSG(("color_space %d not supported", cs));
751 	}
752 }
753 
754