xref: /haiku/src/kits/interface/PrintJob.cpp (revision 1acbe440b8dd798953bec31d18ee589aa3f71b73)
1 /*
2  * Copyright 2001-2006, Haiku.
3  * Distributed under the terms of the MIT license.
4  *
5  * Authors:
6  				I.R. Adema
7  				Stefano Ceccherini (burton666@libero.it)
8  				Michael Pfeiffer
9  */
10 
11 // TODO refactor (avoid code duplications, decrease method sizes)
12 
13 #include <Alert.h>
14 #include <Application.h>
15 #include <Button.h>
16 #include <Debug.h>
17 #include <Entry.h>
18 #include <File.h>
19 #include <FindDirectory.h>
20 #include <Messenger.h>
21 #include <NodeInfo.h>
22 #include <OS.h>
23 #include <Path.h>
24 #include <PrintJob.h>
25 #include <Roster.h>
26 #include <View.h>
27 
28 #include <pr_server.h>
29 
30 #include <stdio.h>
31 #include <stdlib.h>
32 #include <string.h>
33 
34 static const int kSemTimeOut = 50000;
35 
36 static const char *kPrintServerNotRespondingText = "Print Server is not responding.";
37 static const char *kNoPagesToPrintText = "No Pages to print!";
38 
39 // Summery of spool file format:
40 // See articel "How to Write a BeOS R5 Printer Driver" for description
41 // of spool file format: http://haiku-os.org/node/82
42 
43 // print_file_header header
44 // BMessage          job_settings
45 // _page_header_     page_header
46 // followed by number_of_pictures:
47 //   BPoint   where
48 //   BRect    bounds
49 //   BPicture picture
50 // remaining pages start at page_header.next_page of previous page_header
51 
52 struct _page_header_ {
53 	int32 number_of_pictures;
54 	off_t next_page;
55 	int32 reserved[10];
56 };
57 
58 
59 static status_t
60 GetPrinterServerMessenger(BMessenger& messenger)
61 {
62 	messenger = BMessenger(PSRV_SIGNATURE_TYPE);
63 	return messenger.IsValid() ? B_OK : B_ERROR;
64 }
65 
66 
67 static void
68 ShowError(const char *message)
69 {
70 	BAlert* alert = new BAlert("Error", message, "OK");
71 	alert->Go();
72 }
73 
74 
75 namespace BPrivate {
76 
77 	class Configuration {
78 		public:
79 			Configuration(uint32 what, BMessage *input);
80 			~Configuration();
81 
82 			status_t SendRequest(thread_func function);
83 
84 			BMessage* Request();
85 
86 			void SetResult(BMessage* result);
87 			BMessage* Result() const { return fResult; }
88 
89 		private:
90 			void RejectUserInput();
91 			void AllowUserInput();
92 			void DeleteSemaphore();
93 
94 			uint32 fWhat;
95 			BMessage *fInput;
96 			BMessage *fRequest;
97 			BMessage *fResult;
98 			sem_id fThreadCompleted;
99 			BAlert *fHiddenApplicationModalWindow;
100 	};
101 
102 
103 	Configuration::Configuration(uint32 what, BMessage *input)
104 		: fWhat(what),
105 		fInput(input),
106 		fRequest(NULL),
107 		fResult(NULL),
108 		fThreadCompleted(-1),
109 		fHiddenApplicationModalWindow(NULL)
110 	{
111 		RejectUserInput();
112 	}
113 
114 
115 	Configuration::~Configuration()
116 	{
117 		DeleteSemaphore();
118 			// in case SendRequest could not start the thread
119 		delete fRequest; fRequest = NULL;
120 		AllowUserInput();
121 	}
122 
123 
124 	void
125 	Configuration::RejectUserInput()
126 	{
127 		BAlert* alert = new BAlert("bogus", "app_modal_dialog", "OK");
128 		fHiddenApplicationModalWindow = alert;
129 		alert->DefaultButton()->SetEnabled(false);
130 		alert->SetDefaultButton(NULL);
131 		alert->MoveTo(-65000, -65000);
132 		alert->Go(NULL);
133 	}
134 
135 
136 	void
137 	Configuration::AllowUserInput()
138 	{
139 		fHiddenApplicationModalWindow->Lock();
140 		fHiddenApplicationModalWindow->Quit();
141 	}
142 
143 
144 	void
145 	Configuration::DeleteSemaphore()
146 	{
147 		if (fThreadCompleted >= B_OK) {
148 			sem_id id = fThreadCompleted;
149 			fThreadCompleted = -1;
150 			delete_sem(id);
151 		}
152 	}
153 
154 
155 	status_t
156 	Configuration::SendRequest(thread_func function)
157 	{
158 		fThreadCompleted = create_sem(0, "Configuration");
159 		if (fThreadCompleted < B_OK) {
160 			return B_ERROR;
161 		}
162 
163 		thread_id id = spawn_thread(function, "async_request", B_NORMAL_PRIORITY, this);
164 		if (id <= 0 || resume_thread(id) != B_OK) {
165 			return B_ERROR;
166 		}
167 
168 		// Code copied from BAlert::Go()
169 		BWindow* window = dynamic_cast<BWindow*>(BLooper::LooperForThread(find_thread(NULL)));
170 			// Get the originating window, if it exists
171 
172 		// Heavily modified from TextEntryAlert code; the original didn't let the
173 		// blocked window ever draw.
174 		if (window != NULL) {
175 			status_t err;
176 			for (;;) {
177 				do {
178 					err = acquire_sem_etc(fThreadCompleted, 1, B_RELATIVE_TIMEOUT,
179 										  kSemTimeOut);
180 						// We've (probably) had our time slice taken away from us
181 				} while (err == B_INTERRUPTED);
182 
183 				if (err == B_BAD_SEM_ID) {
184 					// Semaphore was finally nuked in SetResult(BMessage *)
185 					break;
186 				}
187 				window->UpdateIfNeeded();
188 			}
189 		} else {
190 			// No window to update, so just hang out until we're done.
191 			while (acquire_sem(fThreadCompleted) == B_INTERRUPTED);
192 		}
193 
194 		status_t status;
195 		wait_for_thread(id, &status);
196 
197 		return Result() != NULL ? B_OK : B_ERROR;
198 	}
199 
200 
201 	BMessage *
202 	Configuration::Request()
203 	{
204 		if (fRequest != NULL)
205 			return fRequest;
206 
207 		if (fInput != NULL) {
208 			fRequest = new BMessage(*fInput);
209 			fRequest->what = fWhat;
210 		} else
211 			fRequest = new BMessage(fWhat);
212 		return fRequest;
213 	}
214 
215 
216 	void
217 	Configuration::SetResult(BMessage *result)
218 	{
219 		fResult = result;
220 		DeleteSemaphore();
221 			// terminate loop in thread spawned by SendRequest
222 	}
223 
224 } // BPrivate
225 
226 
227 BPrintJob::BPrintJob(const char *job_name)
228 	:
229 	fPrintJobName(NULL),
230 	fPageNumber(0),
231 	fSpoolFile(NULL),
232 	fError(B_ERROR),
233 	fSetupMessage(NULL),
234 	fDefaultSetupMessage(NULL),
235 	fCurrentPageHeader(NULL)
236 {
237 	memset(&fCurrentHeader, 0, sizeof(fCurrentHeader));
238 
239 	if (job_name != NULL) {
240 		fPrintJobName = strdup(job_name);
241 	}
242 
243 	fCurrentPageHeader = new _page_header_;
244 	if (fCurrentPageHeader != NULL) {
245 		memset(fCurrentPageHeader, 0, sizeof(*fCurrentPageHeader));
246 	}
247 }
248 
249 
250 BPrintJob::~BPrintJob()
251 {
252 	CancelJob();
253 
254 	if (fPrintJobName != NULL) {
255 		free(fPrintJobName);
256 		fPrintJobName = NULL;
257 	}
258 
259 	delete fDefaultSetupMessage;
260 	fDefaultSetupMessage = NULL;
261 
262 	delete fSetupMessage;
263 	fSetupMessage = NULL;
264 
265 	delete fCurrentPageHeader;
266 	fCurrentPageHeader = NULL;
267 }
268 
269 static
270 status_t ConfigPageThread(void *data)
271 {
272 	BPrivate::Configuration* configuration = static_cast<BPrivate::Configuration*>(data);
273 
274 	BMessenger printServer;
275 	if (GetPrinterServerMessenger(printServer) != B_OK) {
276 		ShowError(kPrintServerNotRespondingText);
277 		configuration->SetResult(NULL);
278 		return B_ERROR;
279 	}
280 
281 	BMessage *request = configuration->Request();
282 	if (request == NULL) {
283 		configuration->SetResult(NULL);
284 		return B_ERROR;
285 	}
286 
287 
288 	BMessage reply;
289 	if (printServer.SendMessage(request, &reply) != B_OK
290 		|| reply.what != 'okok') {
291 		configuration->SetResult(NULL);
292 		return B_ERROR;
293 	}
294 
295 	configuration->SetResult(new BMessage(reply));
296 	return B_OK;
297 }
298 
299 
300 status_t
301 BPrintJob::ConfigPage()
302 {
303 	BPrivate::Configuration configuration(PSRV_SHOW_PAGE_SETUP, fSetupMessage);
304 	status_t status = configuration.SendRequest(ConfigPageThread);
305 	if (status != B_OK)
306 		return status;
307 	delete fSetupMessage;
308 	fSetupMessage = configuration.Result();
309 	HandlePageSetup(fSetupMessage);
310 	return B_OK;
311 }
312 
313 
314 static status_t
315 ConfigJobThread(void *data)
316 {
317 	BPrivate::Configuration* configuration = static_cast<BPrivate::Configuration*>(data);
318 
319 	BMessenger printServer;
320 	if (GetPrinterServerMessenger(printServer) != B_OK) {
321 		ShowError(kPrintServerNotRespondingText);
322 		configuration->SetResult(NULL);
323 		return B_ERROR;
324 	}
325 
326 	BMessage *request = configuration->Request();
327 	if (request == NULL) {
328 		configuration->SetResult(NULL);
329 		return B_ERROR;
330 	}
331 
332 
333 	BMessage reply;
334 	if (printServer.SendMessage(request, &reply) != B_OK
335 		|| reply.what != 'okok') {
336 		configuration->SetResult(NULL);
337 		return B_ERROR;
338 	}
339 
340 	configuration->SetResult(new BMessage(reply));
341 	return B_OK;
342 }
343 
344 status_t
345 BPrintJob::ConfigJob()
346 {
347 	BPrivate::Configuration configuration(PSRV_SHOW_PRINT_SETUP, fSetupMessage);
348 	status_t status = configuration.SendRequest(ConfigJobThread);
349 	if (status != B_OK)
350 		return status;
351 	delete fSetupMessage;
352 	fSetupMessage = configuration.Result();
353 	HandlePrintSetup(fSetupMessage);
354 	return B_OK;
355 }
356 
357 
358 void
359 BPrintJob::BeginJob()
360 {
361 	if (fSpoolFile != NULL) {
362 		// can not start a new job until it has been commited or cancelled
363 		return;
364 	}
365 	if (fCurrentPageHeader == NULL) {
366 		return;
367 	}
368 
369 	if (fSetupMessage == NULL) {
370 		// TODO show alert, setup message is required
371 		return;
372 	}
373 
374 	// create spool file
375 	BPath path;
376 	status_t status = find_directory(B_USER_PRINTERS_DIRECTORY, &path);
377 	if (status != B_OK)
378 		return;
379 
380 	char *printer = GetCurrentPrinterName();
381 	if (printer == NULL)
382 		return;
383 
384 	path.Append(printer);
385 	free(printer);
386 
387 	char mangledName[B_FILE_NAME_LENGTH];
388 	MangleName(mangledName);
389 
390 	path.Append(mangledName);
391 
392 	if (path.InitCheck() != B_OK)
393 		return;
394 
395 	// TODO fSpoolFileName should store the name only (not path which can be 1024 bytes long)
396 	strncpy(fSpoolFileName, path.Path(), sizeof(fSpoolFileName));
397 	fSpoolFile = new BFile(fSpoolFileName, B_READ_WRITE | B_CREATE_FILE);
398 
399 	if (fSpoolFile->InitCheck() != B_OK) {
400 		CancelJob();
401 		return;
402 	}
403 
404 	// add print_file_header
405 	// page_count is updated in CommitJob()
406 	fCurrentHeader.version = 1 << 16;
407 	fCurrentHeader.page_count = 0;
408 
409 	if (fSpoolFile->Write(&fCurrentHeader, sizeof(fCurrentHeader)) != sizeof(fCurrentHeader)) {
410 		CancelJob();
411 		return;
412 	}
413 
414 	// add printer settings message
415 	fSetupMessage->RemoveName(PSRV_FIELD_CURRENT_PRINTER);
416 	fSetupMessage->AddString(PSRV_FIELD_CURRENT_PRINTER, printer);
417 	AddSetupSpec();
418 
419 	// prepare page header
420 	// number_of_pictures is updated in DrawView()
421 	// next_page is updated in SpoolPage()
422 	fCurrentPageHeaderOffset = fSpoolFile->Position();
423 	fCurrentPageHeader->number_of_pictures = 0;
424 
425 	// state variables
426 	fAbort = 0;
427 	fPageNumber = 0;
428 	fError = B_OK;
429 	return;
430 }
431 
432 
433 void
434 BPrintJob::CommitJob()
435 {
436 	if (fSpoolFile == NULL) {
437 		return;
438 	}
439 
440 	if (fPageNumber <= 0) {
441 		ShowError(kNoPagesToPrintText);
442 		CancelJob();
443 		return;
444 	}
445 
446 	if (fCurrentPageHeader->number_of_pictures > 0) {
447 		SpoolPage();
448 	}
449 
450 	// update spool file
451 	EndLastPage();
452 
453 	// set file attributes
454  	app_info appInfo;
455 	be_app->GetAppInfo(&appInfo);
456 	const char* printerName = "";
457 	fSetupMessage->FindString(PSRV_FIELD_CURRENT_PRINTER, &printerName);
458 
459 	BNodeInfo info(fSpoolFile);
460 	info.SetType(PSRV_SPOOL_FILETYPE);
461 
462 	fSpoolFile->WriteAttr(PSRV_SPOOL_ATTR_PAGECOUNT, B_INT32_TYPE, 0, &fPageNumber, sizeof(int32));
463 	fSpoolFile->WriteAttr(PSRV_SPOOL_ATTR_DESCRIPTION, B_STRING_TYPE, 0, fPrintJobName, strlen(fPrintJobName) + 1);
464 	fSpoolFile->WriteAttr(PSRV_SPOOL_ATTR_PRINTER, B_STRING_TYPE, 0, printerName, strlen(printerName) + 1);
465 	fSpoolFile->WriteAttr(PSRV_SPOOL_ATTR_STATUS, B_STRING_TYPE, 0, PSRV_JOB_STATUS_WAITING, strlen(PSRV_JOB_STATUS_WAITING) + 1);
466 	fSpoolFile->WriteAttr(PSRV_SPOOL_ATTR_MIMETYPE, B_STRING_TYPE, 0, appInfo.signature, strlen(appInfo.signature) + 1);
467 
468 	delete fSpoolFile;
469 	fSpoolFile = NULL;
470 	fError = B_ERROR;
471 
472 	// notify print server
473 	BMessenger printServer;
474 	if (GetPrinterServerMessenger(printServer) != B_OK) {
475 		return;
476 	}
477 
478 	BMessage request(PSRV_PRINT_SPOOLED_JOB);
479 	BMessage reply;
480 
481 	request.AddString("JobName", fPrintJobName);
482 	request.AddString("Spool File", fSpoolFileName);
483 	printServer.SendMessage(&request, &reply);
484 }
485 
486 void
487 BPrintJob::CancelJob()
488 {
489 	if (fSpoolFile == NULL) {
490 		return;
491 	}
492 
493 	fAbort = 1;
494 	BEntry(fSpoolFileName).Remove();
495 	delete fSpoolFile;
496 	fSpoolFile = NULL;
497 }
498 
499 
500 void
501 BPrintJob::SpoolPage()
502 {
503 	if (fSpoolFile == NULL) {
504 		return;
505 	}
506 
507 	// update page header
508 	fCurrentPageHeader->next_page = fSpoolFile->Position();
509 	fSpoolFile->Seek(fCurrentPageHeaderOffset, SEEK_SET);
510 	fSpoolFile->Write(fCurrentPageHeader, sizeof(*fCurrentPageHeader));
511 	fSpoolFile->Seek(0, SEEK_END);
512 
513 	fCurrentPageHeader->number_of_pictures = 0;
514 }
515 
516 
517 bool
518 BPrintJob::CanContinue()
519 {
520 	// Check if our local error storage is still B_OK
521 	return fError == B_OK && !fAbort;
522 }
523 
524 
525 void
526 BPrintJob::DrawView(BView *view, BRect rect, BPoint where)
527 {
528 	if (view == NULL)
529 		return;
530 
531 	if (view->LockLooper()) {
532 		BPicture picture;
533 		RecurseView(view, where, &picture, rect);
534 		AddPicture(&picture, &rect, where);
535 		view->UnlockLooper();
536 	}
537 }
538 
539 
540 BMessage *
541 BPrintJob::Settings()
542 {
543 	if (fSetupMessage == NULL) {
544 		return NULL;
545 	}
546 
547 	return new BMessage(*fSetupMessage);
548 }
549 
550 
551 void
552 BPrintJob::SetSettings(BMessage *message)
553 {
554 	if (message != NULL) {
555 		HandlePrintSetup(message);
556 	}
557 	delete fSetupMessage;
558 	fSetupMessage = message;
559 }
560 
561 
562 bool
563 BPrintJob::IsSettingsMessageValid(BMessage *message) const
564 {
565 	char *printerName = GetCurrentPrinterName();
566 	if (printerName == NULL) {
567 		return false;
568 	}
569 
570 	const char *name = NULL;
571 	// The passed message is valid if it contains the right printer name.
572 	bool valid = message != NULL
573 		&& message->FindString("printer_name", &name) == B_OK
574 		&& strcmp(printerName, name) == 0;
575 
576 	free(printerName);
577 
578 	return valid;
579 }
580 
581 // Either SetSettings() or ConfigPage() has to be called prior
582 // to any of the getters otherwise they return undefined values.
583 BRect
584 BPrintJob::PaperRect()
585 {
586 	return fPaperSize;
587 }
588 
589 
590 BRect
591 BPrintJob::PrintableRect()
592 {
593 	return fUsableSize;
594 }
595 
596 
597 void
598 BPrintJob::GetResolution(int32 *xdpi, int32 *ydpi)
599 {
600 	if (xdpi != NULL) {
601 		*xdpi = fXResolution;
602 	}
603 	if (ydpi != NULL) {
604 		*ydpi = fYResolution;
605 	}
606 }
607 
608 
609 int32
610 BPrintJob::FirstPage()
611 {
612     return fFirstPage;
613 }
614 
615 
616 int32
617 BPrintJob::LastPage()
618 {
619     return fLastPage;
620 }
621 
622 
623 int32
624 BPrintJob::PrinterType(void *) const
625 {
626 	BMessenger printServer;
627 	if (GetPrinterServerMessenger(printServer) != B_OK) {
628 		return B_COLOR_PRINTER; // default
629 	}
630 
631 	BMessage message(PSRV_GET_ACTIVE_PRINTER);
632 	BMessage reply;
633 
634 	printServer.SendMessage(&message, &reply);
635 
636 	int32 type;
637 	if (reply.FindInt32("color", &type) != B_OK) {
638 		return B_COLOR_PRINTER; // default
639 	}
640 	return type;
641 }
642 
643 
644 #if 0
645 #pragma mark ----- PRIVATE -----
646 #endif
647 
648 
649 void
650 BPrintJob::RecurseView(BView *view, BPoint origin,
651                        BPicture *picture, BRect rect)
652 {
653 	ASSERT(picture != NULL);
654 
655 	view->AppendToPicture(picture);
656 	view->f_is_printing = true;
657 	view->Draw(rect);
658 	view->f_is_printing = false;
659 	view->EndPicture();
660 
661 	BView *child = view->ChildAt(0);
662 	while (child != NULL) {
663 		// TODO: origin and rect should probably
664 		// be converted for children views in some way
665 		RecurseView(child, origin, picture, rect);
666 		child = child->NextSibling();
667 	}
668 }
669 
670 
671 void
672 BPrintJob::MangleName(char *filename)
673 {
674 	char sysTime[10];
675 	snprintf(sysTime, sizeof(sysTime), "@%lld", system_time() / 1000);
676 	strncpy(filename, fPrintJobName, B_FILE_NAME_LENGTH - sizeof(sysTime));
677 	strcat(filename, sysTime);
678 }
679 
680 
681 void
682 BPrintJob::HandlePageSetup(BMessage *setup)
683 {
684 	setup->FindRect(PSRV_FIELD_PRINTABLE_RECT, &fUsableSize);
685 	setup->FindRect(PSRV_FIELD_PAPER_RECT, &fPaperSize);
686 
687 	// TODO verify data type (taken from libprint)
688 	int64 valueInt64;
689 	if (setup->FindInt64(PSRV_FIELD_XRES, &valueInt64) == B_OK) {
690 		fXResolution = (short)valueInt64;
691 	}
692 	if (setup->FindInt64(PSRV_FIELD_YRES, &valueInt64) == B_OK) {
693 		fYResolution = (short)valueInt64;
694 	}
695 }
696 
697 
698 bool
699 BPrintJob::HandlePrintSetup(BMessage *message)
700 {
701 	HandlePageSetup(message);
702 
703 	bool valid = true;
704 	if (message->FindInt32(PSRV_FIELD_FIRST_PAGE, &fFirstPage) != B_OK) {
705 		valid = false;
706 	}
707 	if (message->FindInt32(PSRV_FIELD_LAST_PAGE, &fLastPage) != B_OK) {
708 		valid = false;
709 	}
710 
711 	return valid;
712 }
713 
714 
715 void
716 BPrintJob::NewPage()
717 {
718 	// write page header
719 	fCurrentPageHeaderOffset = fSpoolFile->Position();
720 	fSpoolFile->Write(fCurrentPageHeader, sizeof(*fCurrentPageHeader));
721 	fPageNumber ++;
722 }
723 
724 
725 void
726 BPrintJob::EndLastPage()
727 {
728 	fSpoolFile->Seek(0, SEEK_SET);
729 	fCurrentHeader.page_count = fPageNumber;
730 	// TODO set first_page correctly
731 	// fCurrentHeader.first_page = 0;
732 	fSpoolFile->Write(&fCurrentHeader, sizeof(fCurrentHeader));}
733 
734 
735 void
736 BPrintJob::AddSetupSpec()
737 {
738 	fSetupMessage->Flatten(fSpoolFile);
739 }
740 
741 
742 void
743 BPrintJob::AddPicture(BPicture *picture, BRect *rect, BPoint where)
744 {
745 	ASSERT(picture != NULL);
746 	ASSERT(fSpoolFile != NULL);
747 	ASSERT(rect != NULL);
748 
749 	if (fCurrentPageHeader->number_of_pictures == 0) {
750 		NewPage();
751 	}
752 	fCurrentPageHeader->number_of_pictures ++;
753 
754 	fSpoolFile->Write(&where, sizeof(where));
755 	fSpoolFile->Write(rect, sizeof(*rect));
756 	picture->Flatten(fSpoolFile);
757 }
758 
759 
760 // Returns a copy of the current printer name
761 // or NULL if it ccould not be obtained.
762 // Caller is responsible to free the string using free().
763 char *
764 BPrintJob::GetCurrentPrinterName() const
765 {
766 	BMessenger printServer;
767 	if (GetPrinterServerMessenger(printServer)) {
768 		return NULL;
769 	}
770 
771 	BMessage message(PSRV_GET_ACTIVE_PRINTER);
772 	BMessage reply;
773 
774 	const char *printerName = NULL;
775 
776 	if (printServer.SendMessage(&message, &reply) == B_OK) {
777 		reply.FindString("printer_name", &printerName);
778 	}
779 	if (printerName == NULL) {
780 		return NULL;
781 	}
782 	return strdup(printerName);
783 }
784 
785 
786 void
787 BPrintJob::LoadDefaultSettings()
788 {
789 	BMessenger printServer;
790 	if (GetPrinterServerMessenger(printServer) != B_OK) {
791 		return;
792 	}
793 
794 	BMessage message(PSRV_GET_DEFAULT_SETTINGS);
795 	BMessage *reply = new BMessage;
796 
797 	printServer.SendMessage(&message, reply);
798 
799 	HandlePrintSetup(reply);
800 
801 	delete fDefaultSetupMessage;
802 	fDefaultSetupMessage = reply;
803 }
804 
805 
806 void BPrintJob::_ReservedPrintJob1() {}
807 void BPrintJob::_ReservedPrintJob2() {}
808 void BPrintJob::_ReservedPrintJob3() {}
809 void BPrintJob::_ReservedPrintJob4() {}
810