All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hp.jipp.pdl.pclm.PclmWriter.kt Maven / Gradle / Ivy

There is a newer version: 0.7.16
Show newest version
// Copyright 2018 HP Development Company, L.P.
// SPDX-License-Identifier: MIT

package com.hp.jipp.pdl.pclm

import com.hp.jipp.model.PclmRasterBackSide
import com.hp.jipp.model.Sides
import com.hp.jipp.pdl.ColorSpace
import com.hp.jipp.pdl.RenderableDocument
import com.hp.jipp.pdl.RenderablePage
import com.hp.jipp.pdl.isEven
import java.io.ByteArrayOutputStream
import java.io.CharArrayWriter
import java.io.IOException
import java.io.OutputStream
import java.io.Writer
import java.util.zip.DeflaterOutputStream
import kotlin.math.roundToInt

/**
 * Writes a document using PCLm page description language as specified in "Wi-Fi Peer-to-Peer
 * Services Print Technical Specification v1.1".
 *
 * This format is a strict subset of PDF, so a PCLm file can be opened and inspected by ordinary
 * PDF viewers.
 *
 * IMPORTANT: This implementation does NOT yet support Duplex use cases.
 *
 * See also: https://wwwimages2.adobe.com/content/dam/acom/en/devnet/pdf/PDF32000_2008.pdf
 */
@Suppress("LargeClass", "TooManyFunctions") // Clearer for now to have it all in one place
class PclmWriter(
    /** Destination for encoded PCLM stream */
    private val outputStream: OutputStream,
    /** PCLM Settings to use when writing. */
    private val settings: PclmSettings
) : Writer() {
    /** Tracks the number of bytes written to the output stream so far */
    private var written: Int = 0

    /** Object number of each page object added to the stream */
    private val pageObjectNumbers = ArrayList()

    /** A list of offsets to each sequentially-numbered object in the stream */
    private val crossReferences = ArrayList()

    override fun flush() {
        outputStream.flush()
    }

    override fun close() {
        outputStream.close()
    }

    override fun write(chars: CharArray, offset: Int, length: Int) {
        written += length
        outputStream.write(String(chars).toByteArray(), offset, length)
    }

    /** Write a complete document in PCLm format */
    fun write(document: RenderableDocument) {
        crossReferences.add(-1) // Placeholder so that objects begin with 1
        crossReferences.add(-1) // Placeholder for Catalog
        crossReferences.add(-1) // Placeholder for Page Tree
        startDoc()
        document.mapPages { doc ->
            doc.mapIndexed { pageNumber, page -> page.transform(pageNumber) }
        }.handleSides(settings.output).forEach { page ->
            writePage(document, page)
        }
        endDoc()
        flush()
    }

    private fun RenderablePage.transform(number: Int) =
        when {
            number.isEven -> this
            settings.output.sides == Sides.twoSidedLongEdge && settings.backSide == PclmRasterBackSide.rotated ->
                rotated()
            settings.output.sides == Sides.twoSidedLongEdge && settings.backSide == PclmRasterBackSide.flipped ->
                flipY()
            settings.output.sides == Sides.twoSidedShortEdge && settings.backSide == PclmRasterBackSide.flipped ->
                flipX()
            else -> this
        }

    /** Write the header to start a PCLM */
    private fun startDoc() {
        write("%PDF-1.7\n")
        write("%PCLm 1.0\n") // Spec shows PCLm-1.0 but this doesn't work
    }

    private data class Swath(val objectNumber: Int, val imageNumber: Int, val height: Int, val yOffset: Int)

    /** Write a rendered page */
    private fun writePage(doc: RenderableDocument, page: RenderablePage) {
        // Number of strips (rounding up so we get all strips including the last)
        val stripCount = Math.ceil(page.heightPixels / settings.stripHeight.toDouble()).roundToInt()

        // Find the top of the page (PDF puts y=0 at bottom, opposite of most computer images)
        var yOffset = 0

        // Build out swath definitions. Note: duplex will run backwards
        val swaths = (0 until stripCount).map { index ->
            val height = if (index == stripCount - 1) {
                page.heightPixels - (stripCount - 1) * settings.stripHeight
            } else settings.stripHeight

            // Predict the object number assuming page object, page content stream object, and interleaved transforms
            Swath(objectNumber = crossReferences.size + 2 + (index * 2),
                    imageNumber = index,
                    height = height,
                    yOffset = yOffset.also { yOffset += height })
        }

        // Write the page object
        val pageObjectNumber = pdObject {
            write("/Type /Page\n")
            write("/Parent $PAGE_TREE_OBJECT_NUMBER 0 R\n")
            write("/Resources <<\n")
            write("/XObject <<\n")
            for (swath in swaths) {
                write("/Image${swath.imageNumber} ${swath.objectNumber} 0 R\n")
            }
            write(">>\n")
            write(">>\n")

            // Note: For landscape we would use a different media box
            // MediaBox MUST be integral for some printers
            val heightPoints: Int = page.heightPixels * POINTS_PER_INCH / doc.dpi
            val widthPoints: Int = page.widthPixels * POINTS_PER_INCH / doc.dpi
            write("/MediaBox [ 0 0 $widthPoints $heightPoints ]\n")
            write("/Contents [ ${objectNumber + 1} 0 R ]\n")
        }

        pageObjectNumbers += pageObjectNumber

        // Write the page content stream object
        pdObject {
            val contentStream = CharArrayWriter()
            contentStream.write("${POINTS_PER_INCH.toDouble() / doc.dpi} 0 0 " +
                "${POINTS_PER_INCH.toDouble() / doc.dpi} 0 0 cm\n")
            contentStream.write("/P <> BDC q\n")

            for (swath in swaths) {
                // Transfer swath image offset back to page coordinates
                val pageYOffset = page.heightPixels - swath.yOffset - swath.height
                contentStream.write("${page.widthPixels} 0 0 ${swath.height} 0 $pageYOffset cm\n")
                contentStream.write("/Image${swath.imageNumber} Do Q\n")
                contentStream.write("/P <> BDC q\n")
            }

            charStream = contentStream.toCharArray()

            write("/Length ${contentStream.size()}\n")
        }
        writeSwaths(page, swaths, page.widthPixels)
    }

    private fun writeSwaths(page: RenderablePage, swaths: List, widthPixels: Int) {
        var first = true
        for (swath in swaths) {
            var rawBytes: ByteArray? = null
            pdObject {
                val curBytes = getBytes(page, swath, first, widthPixels, rawBytes)
                rawBytes = curBytes // Maintain for possible re-use
                byteStream = if (curBytes.isNotEmpty()) {
                    // Note: Consider jpg instead of flate, but switching between the two could look weird.
                    val encoded = ByteArrayOutputStream()
                    DeflaterOutputStream(encoded).use { it.write(rawBytes) }
                    encoded.toByteArray()
                } else {
                    rawBytes
                }

                write("/Width $widthPixels\n")
                when (settings.output.colorSpace) {
                    ColorSpace.Rgb -> write("/ColorSpace /DeviceRGB\n")
                    ColorSpace.Grayscale -> write("/ColorSpace /DeviceGray\n")
                }

                write("/Height ${swath.height}\n")
                write("/Filter /FlateDecode\n")
                write("/Subtype /Image\n")
                write("/Length ${byteStream!!.size}\n")
                write("/Type /XObject\n")
                write("/BitsPerComponent 8\n")
                if (curBytes.isEmpty() && !first) {
                    write("/Name /WhiteStrip\n")
                } else {
                    write("/Name /ColorStrip\n")
                }
            }

            // Note: Transform first when duplex (not yet implemented)
            pdObject {
                byteStream = "q /Image Do Q\n".toByteArray()
                write("/Length ${byteStream!!.size}\n")
            }

            flush()
            first = false
        }
    }

    /** Obtain bytes from a renderable page */
    private fun getBytes(
        page: RenderablePage,
        swath: Swath,
        forceNonBlank: Boolean,
        widthPixels: Int,
        bytes: ByteArray?
    ): ByteArray {
        var renderBytes = bytes
        val size = swath.height * widthPixels * settings.output.colorSpace.bytesPerPixel
        if (renderBytes?.size != size) {
            renderBytes = ByteArray(size)
        }

        page.render(yOffset = swath.yOffset, swathHeight = swath.height, colorSpace = settings.output.colorSpace,
            byteArray = renderBytes)

        if (forceNonBlank) return renderBytes

        // Check to see if blank
        val blank: Byte = BLANK
        return if (renderBytes.firstOrNull { it != blank } == null) {
            // All bytes are blank so return an empty array
            ByteArray(0)
        } else {
            // At least one non-blank byte so return normal array
            renderBytes
        }
    }

    /** Write the components necessary to end the PCLM file */
    private fun endDoc() {
        // Write the Catalog
        pdObject(CATALOG_OBJECT_NUMBER) {
            write("/Type /Catalog\n")
            // The page tree is next
            write("/Pages ${objectNumber + 1} 0 R\n")
        }

        // Write the Page Tree
        pdObject(PAGE_TREE_OBJECT_NUMBER) {
            write("/Count ${pageObjectNumbers.size}\n")
            write("/Kids [ ")
            for (i in 0 until pageObjectNumbers.size) {
                write("${pageObjectNumbers[i]} 0 R ")
            }
            write("]\n")
            write("/Type /Pages\n")
        }

        // Write the cross-references (skip the first placeholder)
        val xrefOffset = written
        write("xref\n")
        write("1 ${crossReferences.size - 1}\n")
        for (offset in crossReferences.drop(1)) {
            write(String.format("%010d 00000 n \n", offset))
        }

        // Write the trailer
        write("trailer\n")
        write("<<\n")
        write("/Size ${crossReferences.size - 1}\n")
        write("/Root $CATALOG_OBJECT_NUMBER 0 R\n")
        write(">>\n")
        write("startxref\n")
        write("$xrefOffset\n")
        write("%%EOF\n")
    }

    /** Create an object in the output stream, returning its object number */
    private fun pdObject(objectNumber: Int = crossReferences.size, block: InObject.() -> Unit): Int {
        val context = InObject(objectNumber)
        when {
            objectNumber < crossReferences.size -> crossReferences[objectNumber] = written
            objectNumber == crossReferences.size -> crossReferences += written
            else -> throw IOException("Cannot create object beyond last position")
        }

        write("${context.objectNumber} 0 obj\n")
        write("<<\n")
        context.block()
        write(">>\n")
        if (context.byteStream != null) {
            write("stream\n")
            flush()
            outputStream.write(context.byteStream!!)
            written += context.byteStream!!.size
            write("\nendstream\n")
        } else if (context.charStream != null) {
            write("stream\n")
            flush()
            write(context.charStream!!)
            write("\nendstream\n")
        }
        write("endobj\n")
        return objectNumber
    }

    private class InObject(val objectNumber: Int) {
        /** An array of bytes to include with this object as a stream */
        var byteStream: ByteArray? = null
        /** An array of chars to encode UTF-8 and include with this object as a stream */
        var charStream: CharArray? = null
    }

    private companion object {
        private const val BLANK = 0xFF.toByte()
        private const val POINTS_PER_INCH: Int = 72
        private const val CATALOG_OBJECT_NUMBER = 1
        private const val PAGE_TREE_OBJECT_NUMBER = CATALOG_OBJECT_NUMBER + 1
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy