چاپ اسناد سفارشی

برای برخی‌از برنامه‌ها، مثل برنامه‌های طراحی، برنامه‌های چیدمان صفحه، و برنامه‌های دیگری که بر خروجی گرافیکی تمرکز دارند، ایجاد صفحه‌های چاپی زیبا ویژگی کلیدی است. در این مورد، چاپ کردن تصویر یا سند HTML کافی نیست. خروجی چاپی برای این نوع برنامه‌ها به کنترل دقیق همه چیزهایی که در صفحه قرار می‌گیرد، ازجمله قلم‌ها، جریان نوشتار، جداسازهای صفحه، سرصفحه‌ها، پاصفحه‌ها، و عناصر گرافیکی نیاز دارد.

ایجاد خروجی چاپی که کاملاً برای برنامه شما سفارشی شده باشد، نسبت‌به رویکردهایی که قبلاً بحث شد، به سرمایه‌گذاری برنامه‌نویسی بیشتری نیاز دارد. باید مؤلفه‌هایی بسازید که با چارچوب چاپ ارتباط برقرار کنند، با تنظیمات چاپگر سازگار شوند، عناصر صفحه را ترسیم کنند، و چاپ را در چندین صفحه مدیریت کنند.

این درس به شما نشان می‌دهد که چگونه با مدیر چاپ ارتباط برقرار کنید، یک آداپتور چاپ ایجاد کنید و محتوا برای چاپ بسازید.

وقتی برنامه‌تان فرایند چاپ را مستقیماً مدیریت می‌کند، اولین قدم پس‌از دریافت درخواست چاپ از کاربر، متصل شدن به چارچوب چاپ Android و دریافت نمونه‌ای از کلاس PrintManager است. این کلاس به شما امکان می‌دهد کار چاپی را مقداردهی اولیه کنید و چرخه حیات چاپ را شروع کنید. نمونه کد زیر نحوه دریافت مدیر چاپ و شروع فرایند چاپ را نشان می‌دهد.

کاتلین

private fun doPrint() {
    activity?.also { context ->
        // Get a PrintManager instance
        val printManager = context.getSystemService(Context.PRINT_SERVICE) as PrintManager
        // Set job name, which will be displayed in the print queue
        val jobName = "${context.getString(R.string.app_name)} Document"
        // Start a print job, passing in a PrintDocumentAdapter implementation
        // to handle the generation of a print document
        printManager.print(jobName, MyPrintDocumentAdapter(context), null)
    }
}

جاوا

private void doPrint() {
    // Get a PrintManager instance
    PrintManager printManager = (PrintManager) getActivity()
            .getSystemService(Context.PRINT_SERVICE);

    // Set job name, which will be displayed in the print queue
    String jobName = getActivity().getString(R.string.app_name) + " Document";

    // Start a print job, passing in a PrintDocumentAdapter implementation
    // to handle the generation of a print document
    printManager.print(jobName, new MyPrintDocumentAdapter(getActivity()),
            null); //
}

کد نمونه بالا نشان می‌دهد که چگونه یک کار چاپ را نام‌گذاری کنید و نمونه‌ای از کلاس PrintDocumentAdapter را تنظیم کنید که مراحل چرخه حیات چاپ را مدیریت می‌کند. پیاده‌سازی کلاس آداپتور چاپ در بخش بعدی مورد بحث قرار می‌گیرد.

توجه: آخرین پارامتر در روش print() یک شیء PrintAttributes می‌گیرد. می‌توانید از این پارامتر برای ارائه راهنمایی به چارچوب چاپ و گزینه‌های ازپیش تنظیم‌شده براساس چرخه چاپ قبلی استفاده کنید و ازاین‌طریق تجربه کاربری را بهبود ببخشید. همچنین می‌توانید از این پارامتر برای تنظیم گزینه‌هایی استفاده کنید که برای محتوای درحال چاپ مناسب‌تر هستند، مثلاً تنظیم جهت روی افقی هنگام چاپ عکسی که در آن جهت است.

آداپتور چاپ با چارچوب چاپ Android تعامل دارد و مراحل فرایند چاپ را مدیریت می‌کند. این فرایند مستلزم آن است که کاربران قبل‌از ایجاد سند برای چاپ، چاپگرها و گزینه‌های چاپ را انتخاب کنند. این انتخاب‌ها می‌تواند بر خروجی نهایی تأثیر بگذارد، زیرا کاربر چاپگرهایی با قابلیت‌های خروجی متفاوت، اندازه‌های صفحه متفاوت، یا جهت‌های صفحه متفاوت را انتخاب می‌کند. وقتی این انتخاب‌ها انجام شد، چارچوب چاپ از آداپتور شما می‌خواهد که یک سند چاپی را برای آماده‌سازی خروجی نهایی طرح‌بندی و تولید کند. وقتی کاربر روی دکمه چاپ ضربه می‌زند، چارچوب سند چاپ نهایی را می‌گیرد و آن را برای خروجی به ارائه‌دهنده چاپ ارسال می‌کند. درطول فرایند چاپ، کاربران می‌توانند کنش چاپ را لغو کنند، بنابراین آداپتور چاپ شما باید درخواست‌های لغو را نیز دریافت کند و به آن‌ها واکنش نشان دهد.

کلاس انتزاعی PrintDocumentAdapter برای مدیریت چرخه حیات چاپ طراحی شده است که چهار روش اصلی فراخوانی دارد. برای تعامل صحیح با چارچوب چاپ، باید این روش‌ها را در آداپتور چاپ خود پیاده‌سازی کنید:

  • onStart() - یک‌بار در شروع فرایند چاپ فراخوانده می‌شود. اگر برنامه‌تان وظایف آماده‌سازی یک‌باره‌ای برای اجرا دارد، مثلاً گرفتن عکس آنی از داده‌هایی که باید چاپ شود، آن‌ها را اینجا اجرا کنید. پیاده‌سازی این روش در آداپتور شما الزامی نیست.
  • onLayout() - هربار که کاربر تنظیمات چاپی را که بر خروجی تأثیر می‌گذارد تغییر می‌دهد، مانند اندازه صفحه متفاوت یا جهت صفحه، فراخوانی می‌شود و به برنامه شما فرصت می‌دهد تا چیدمان صفحاتی را که باید چاپ شوند محاسبه کند. این روش باید حداقل تعداد صفحه‌های موردانتظار در سند چاپی را برگرداند.
  • onWrite() - برای تبدیل صفحه‌های چاپ‌شده به فایلی برای چاپ فراخوانی شد. این روش ممکن است یک یا چند بار پس‌از هر تماس onLayout() فراخوانی شود.
  • onFinish() - یک‌بار در پایان فرایند چاپ فراخوانی می‌شود. اگر برنامه‌تان وظایف یک‌باره‌ای برای انجام دارد، آن‌ها را در اینجا اجرا کنید. پیاده‌سازی این روش در آداپتور شما الزامی نیست.

بخش‌های زیر نحوه پیاده‌سازی روش‌های طرح‌بندی و نوشتن را شرح می‌دهند که برای عملکرد آداپتور چاپ حیاتی هستند.

توجه: این روش‌های آداپتور در رشته اصلی برنامه شما فراخوانی می‌شوند. اگر انتظار دارید اجرای این روش‌ها در پیاده‌سازی شما زمان قابل‌توجهی طول بکشد، آن‌ها را برای اجرا در رشته‌ای جداگانه پیاده‌سازی کنید. برای مثال، می‌توانید کار نوشتن سند چیدمان یا چاپ را در اشیاء AsyncTask جداگانه کپسوله‌سازی کنید.

محاسبه اطلاعات سند چاپی

در پیاده‌سازی کلاس PrintDocumentAdapter، برنامه شما باید بتواند نوع سندی را که ایجاد می‌کند مشخص کند و تعداد کل صفحه‌ها را برای کار چاپ، بااستفاده از اطلاعات مربوط به اندازه صفحه چاپ‌شده، محاسبه کند. پیاده‌سازی روش onLayout() در آداپتور این محاسبات را انجام می‌دهد و اطلاعاتی درباره خروجی موردانتظار کار چاپ در کلاس PrintDocumentInfo ارائه می‌دهد، ازجمله تعداد صفحه‌ها و نوع محتوا. نمونه کد زیر پیاده‌سازی پایه روش onLayout() را برای PrintDocumentAdapter نشان می‌دهد:

کاتلین

override fun onLayout(
        oldAttributes: PrintAttributes?,
        newAttributes: PrintAttributes,
        cancellationSignal: CancellationSignal?,
        callback: LayoutResultCallback,
        extras: Bundle?
) {
    // Create a new PdfDocument with the requested page attributes
    pdfDocument = PrintedPdfDocument(activity, newAttributes)

    // Respond to cancellation request
    if (cancellationSignal?.isCanceled == true) {
        callback.onLayoutCancelled()
        return
    }

    // Compute the expected number of printed pages
    val pages = computePageCount(newAttributes)

    if (pages > 0) {
        // Return print information to print framework
        PrintDocumentInfo.Builder("print_output.pdf")
                .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
                .setPageCount(pages)
                .build()
                .also { info ->
                    // Content layout reflow is complete
                    callback.onLayoutFinished(info, true)
                }
    } else {
        // Otherwise report an error to the print framework
        callback.onLayoutFailed("Page count calculation failed.")
    }
}

جاوا

@Override
public void onLayout(PrintAttributes oldAttributes,
                     PrintAttributes newAttributes,
                     CancellationSignal cancellationSignal,
                     LayoutResultCallback callback,
                     Bundle metadata) {
    // Create a new PdfDocument with the requested page attributes
    pdfDocument = new PrintedPdfDocument(getActivity(), newAttributes);

    // Respond to cancellation request
    if (cancellationSignal.isCanceled() ) {
        callback.onLayoutCancelled();
        return;
    }

    // Compute the expected number of printed pages
    int pages = computePageCount(newAttributes);

    if (pages > 0) {
        // Return print information to print framework
        PrintDocumentInfo info = new PrintDocumentInfo
                .Builder("print_output.pdf")
                .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
                .setPageCount(pages)
                .build();
        // Content layout reflow is complete
        callback.onLayoutFinished(info, true);
    } else {
        // Otherwise report an error to the print framework
        callback.onLayoutFailed("Page count calculation failed.");
    }
}

اجرای روش onLayout() می‌تواند سه نتیجه داشته باشد: تکمیل، لغو، یا شکست در مواردی که محاسبه چیدمان تکمیل نمی‌شود. باید یکی از این نتایج را با فراخوانی روش مناسب شیء PrintDocumentAdapter.LayoutResultCallback نشان دهید.

توجه: پارامتر بولی روش onLayoutFinished() نشان می‌دهد که آیا محتوای چیدمان از آخرین درخواست واقعاً تغییر کرده است یا نه. تنظیم صحیح این پارامتر به چارچوب چاپ اجازه می‌دهد تا از فراخوانی غیرضروری روش onWrite() جلوگیری کند، اساساً سند چاپ قبلاً نوشته‌شده را ذخیره کند و عملکرد را بهبود بخشد.

کار اصلی onLayout() محاسبه تعداد صفحاتی است که با توجه به ویژگی‌های چاپگر به‌عنوان خروجی انتظار می‌رود. نحوه محاسبه این عدد به‌شدت به نحوه چیدمان صفحه‌ها برای چاپ توسط برنامه شما بستگی دارد. نمونه کد زیر پیاده‌سازی‌ای را نشان می‌دهد که در آن تعداد صفحه‌ها براساس جهت چاپ تعیین می‌شود:

کاتلین

private fun computePageCount(printAttributes: PrintAttributes): Int {
    var itemsPerPage = 4 // default item count for portrait mode

    val pageSize = printAttributes.mediaSize
    if (!pageSize.isPortrait) {
        // Six items per page in landscape orientation
        itemsPerPage = 6
    }

    // Determine number of print items
    val printItemCount: Int = getPrintItemCount()

    return Math.ceil((printItemCount / itemsPerPage.toDouble())).toInt()
}

جاوا

private int computePageCount(PrintAttributes printAttributes) {
    int itemsPerPage = 4; // default item count for portrait mode

    MediaSize pageSize = printAttributes.getMediaSize();
    if (!pageSize.isPortrait()) {
        // Six items per page in landscape orientation
        itemsPerPage = 6;
    }

    // Determine number of print items
    int printItemCount = getPrintItemCount();

    return (int) Math.ceil(printItemCount / itemsPerPage);
}

نوشتن فایل سند چاپی

وقتی زمان نوشتن خروجی چاپ در فایل فرامی‌رسد، چارچوب چاپ Android روش onWrite() کلاس PrintDocumentAdapter برنامه شما را فراخوانی می‌کند. پارامترهای روش مشخص می‌کنند کدام صفحه‌ها باید نوشته شوند و از کدام فایل خروجی استفاده شود. سپس پیاده‌سازی شما از این روش باید هر صفحه محتوای درخواست‌شده را در یک فایل سند PDF چندصفحه‌ای ارائه دهد. وقتی این فرایند تکمیل شد، روش onWriteFinished() شیء فراخوانی را صدا می‌زنید.

توجه: چارچوب چاپ Android ممکن است روش onWrite() را یک یا چند بار برای هر فراخوانی به onLayout() فراخوانی کند. به همین دلیل، مهم است که پارامتر بولی روش onLayoutFinished() را روی false تنظیم کنید وقتی چیدمان محتوای چاپ تغییر نکرده است، تا از بازنویسی‌های غیرضروری سند چاپ جلوگیری شود.

توجه: پارامتر بولی روش onLayoutFinished() نشان می‌دهد که آیا محتوای چیدمان از آخرین درخواست واقعاً تغییر کرده است یا نه. تنظیم صحیح این پارامتر به چارچوب چاپ اجازه می‌دهد از فراخوانی غیرضروری روش onLayout() اجتناب کند، اساساً سند چاپ قبلاً نوشته‌شده را ذخیره کند و عملکرد را بهبود بخشد.

نمونه زیر سازوکار اصلی این فرایند را بااستفاده از کلاس PrintedPdfDocument برای ایجاد فایل PDF نشان می‌دهد:

کاتلین

override fun onWrite(
        pageRanges: Array<out PageRange>,
        destination: ParcelFileDescriptor,
        cancellationSignal: CancellationSignal?,
        callback: WriteResultCallback
) {
    // Iterate over each page of the document,
    // check if it's in the output range.
    for (i in 0 until totalPages) {
        // Check to see if this page is in the output range.
        if (containsPage(pageRanges, i)) {
            // If so, add it to writtenPagesArray. writtenPagesArray.size()
            // is used to compute the next output page index.
            writtenPagesArray.append(writtenPagesArray.size(), i)
            pdfDocument?.startPage(i)?.also { page ->

                // check for cancellation
                if (cancellationSignal?.isCanceled == true) {
                    callback.onWriteCancelled()
                    pdfDocument?.close()
                    pdfDocument = null
                    return
                }

                // Draw page content for printing
                drawPage(page)

                // Rendering is complete, so page can be finalized.
                pdfDocument?.finishPage(page)
            }
        }
    }

    // Write PDF document to file
    try {
        pdfDocument?.writeTo(FileOutputStream(destination.fileDescriptor))
    } catch (e: IOException) {
        callback.onWriteFailed(e.toString())
        return
    } finally {
        pdfDocument?.close()
        pdfDocument = null
    }
    val writtenPages = computeWrittenPages()
    // Signal the print framework the document is complete
    callback.onWriteFinished(writtenPages)

    ...
}

جاوا

@Override
public void onWrite(final PageRange[] pageRanges,
                    final ParcelFileDescriptor destination,
                    final CancellationSignal cancellationSignal,
                    final WriteResultCallback callback) {
    // Iterate over each page of the document,
    // check if it's in the output range.
    for (int i = 0; i < totalPages; i++) {
        // Check to see if this page is in the output range.
        if (containsPage(pageRanges, i)) {
            // If so, add it to writtenPagesArray. writtenPagesArray.size()
            // is used to compute the next output page index.
            writtenPagesArray.append(writtenPagesArray.size(), i);
            PdfDocument.Page page = pdfDocument.startPage(i);

            // check for cancellation
            if (cancellationSignal.isCanceled()) {
                callback.onWriteCancelled();
                pdfDocument.close();
                pdfDocument = null;
                return;
            }

            // Draw page content for printing
            drawPage(page);

            // Rendering is complete, so page can be finalized.
            pdfDocument.finishPage(page);
        }
    }

    // Write PDF document to file
    try {
        pdfDocument.writeTo(new FileOutputStream(
                destination.getFileDescriptor()));
    } catch (IOException e) {
        callback.onWriteFailed(e.toString());
        return;
    } finally {
        pdfDocument.close();
        pdfDocument = null;
    }
    PageRange[] writtenPages = computeWrittenPages();
    // Signal the print framework the document is complete
    callback.onWriteFinished(writtenPages);

    ...
}

این نمونه، رندر کردن محتوای صفحه PDF را به روش drawPage() واگذار می‌کند که در بخش بعدی مورد بحث قرار گرفته است.

همانند چیدمان، اجرای روش onWrite() می‌تواند سه نتیجه داشته باشد: تکمیل، لغو، یا شکست در شرایطی که محتوا قابل‌نوشتن نباشد. باید یکی از این نتایج را با فراخوانی روش مناسب شیء PrintDocumentAdapter.WriteResultCallback نشان دهید.

توجه: پردازش سند برای چاپ می‌تواند عملیاتی باشد که به منابع زیادی نیاز دارد. برای جلوگیری از مسدود شدن رشته میانای کاربر اصلی برنامه‌تان، بهتر است عملیات رندر کردن و نوشتن صفحه را در رشته‌ای جداگانه انجام دهید، برای مثال در AsyncTask. برای کسب اطلاعات بیشتر درباره کار با رشته‌های اجرایی مانند تکالیف ناهمگام، به فرایندها و رشته‌ها مراجعه کنید.

درحال رسم محتوای صفحه PDF

وقتی برنامه‌تان چاپ می‌کند، برنامه‌تان باید سند PDF تولید کند و آن را برای چاپ به چارچوب چاپ Android ارسال کند. می‌توانید از هر کتابخانه تولید PDF برای این منظور استفاده کنید. این درس نشان می‌دهد چگونه از کلاس PrintedPdfDocument برای تولید صفحه‌های PDF از محتوایتان استفاده کنید.

کلاس PrintedPdfDocument از شیء Canvas برای رسم عناصر در صفحه PDF استفاده می‌کند، مشابه رسم در چیدمان فعالیت. می‌توانید بااستفاده از روش‌های رسم Canvas، عناصر را روی صفحه چاپ‌شده رسم کنید. کد نمونه زیر نشان می‌دهد که چگونه بااستفاده از این روش‌ها، برخی عناصر ساده را در صفحه سند PDF ترسیم کنید:

کاتلین

private fun drawPage(page: PdfDocument.Page) {
    page.canvas.apply {

        // units are in points (1/72 of an inch)
        val titleBaseLine = 72f
        val leftMargin = 54f

        val paint = Paint()
        paint.color = Color.BLACK
        paint.textSize = 36f
        drawText("Test Title", leftMargin, titleBaseLine, paint)

        paint.textSize = 11f
        drawText("Test paragraph", leftMargin, titleBaseLine + 25, paint)

        paint.color = Color.BLUE
        drawRect(100f, 100f, 172f, 172f, paint)
    }
}

جاوا

private void drawPage(PdfDocument.Page page) {
    Canvas canvas = page.getCanvas();

    // units are in points (1/72 of an inch)
    int titleBaseLine = 72;
    int leftMargin = 54;

    Paint paint = new Paint();
    paint.setColor(Color.BLACK);
    paint.setTextSize(36);
    canvas.drawText("Test Title", leftMargin, titleBaseLine, paint);

    paint.setTextSize(11);
    canvas.drawText("Test paragraph", leftMargin, titleBaseLine + 25, paint);

    paint.setColor(Color.BLUE);
    canvas.drawRect(100, 100, 172, 172, paint);
}

هنگام استفاده از Canvas برای طراحی روی صفحه PDF، عناصر برحسب نقطه مشخص می‌شوند که ۱/۷۲ اینچ است. مطمئن شوید که از این واحد اندازه‌گیری برای مشخص کردن اندازه عناصر در صفحه استفاده می‌کنید. برای موقعیت‌یابی عناصر ترسیم‌شده، سیستم مختصات از 0,0 برای گوشه بالا سمت چپ صفحه شروع می‌شود.

نکته: اگرچه شیء Canvas به شما امکان می‌دهد عناصر چاپی را در لبه سند PDF قرار دهید، بسیاری از چاپگرها نمی‌توانند تا لبه کاغذ فیزیکی چاپ کنند. مطمئن شوید که هنگام ساختن سند چاپی با این کلاس، لبه‌های غیرقابل‌چاپ صفحه را درنظر می‌گیرید.