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

commonMain.org.jetbrains.letsPlot.util.pngj.ChunkReader.kt Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2023 JetBrains s.r.o.
 * Use of this source code is governed by the MIT license that can be found in the LICENSE file.
 *
 * This file has been modified by JetBrains : Java code has been converted to Kotlin code.
 *
 * THE FOLLOWING IS THE COPYRIGHT OF THE ORIGINAL DOCUMENT:
 *
 * Copyright (c) 2009-2012, Hernán J. González.
 * Licensed under the Apache License, Version 2.0.
 *
 * The original PNGJ library is written in Java and can be found here: [PNGJ](https://github.com/leonbloy/pngj).
 */

@file:Suppress("KDocUnresolvedReference")

package org.jetbrains.letsPlot.util.pngj

import org.jetbrains.letsPlot.util.pngj.ChunkReader.ChunkReaderMode
import org.jetbrains.letsPlot.util.pngj.chunks.ChunkRaw

/**
 * Parses a PNG chunk, consuming bytes in one of three modes:
 * [ChunkReaderMode.BUFFER], [ChunkReaderMode.PROCESS],
 * [ChunkReaderMode.SKIP].
 *
 *
 * It calls [.chunkDone] when done. Also calls
 * [.processData] if `PROCESS` mode. Apart
 * from thas, it's totally agnostic (it doesn't know about IDAT chunks, or PNG
 * general structure)
 *
 *
 * The object wraps a ChunkRaw instance (content allocated and filled
 * only if BUFFER mode). It should be short lived (one instance created for each
 * chunk, and discarded after reading), but the wrapped chunkRaw can be
 * (usually is) long lived.
 */
internal abstract class ChunkReader(clen: Int, id: String, offsetInPng: Long, mode: ChunkReaderMode?) : IBytesConsumer {
    /**
     * see [ChunkReaderMode]
     */
    val mode: ChunkReaderMode

    /**
     * Returns raw chunk (data can be empty or not, depending on
     * ChunkReaderMode)
     *
     * @return Raw chunk - never null
     */
    val chunkRaw: ChunkRaw

    /**
     * How many bytes have been read for this chunk, data only
     */
    protected var read = 0
    private var crcn = 0 // how many bytes have been read from crc
    private var crcCheck // by default, this is false for SKIP, true elsewhere
            : Boolean
    protected var errorBehav: ErrorBehaviour = ErrorBehaviour.STRICT

    /**
     * Modes of ChunkReader chunk processing.
     */
    enum class ChunkReaderMode {
        /**
         * Stores full chunk data in buffer
         */
        BUFFER,

        /**
         * Does not store content, processes on the fly, calling processData()
         * for each partial read
         */
        PROCESS,

        /**
         * Does not store nor process - implies crcCheck=false (by default).
         */
        SKIP
    }

    /**
     * The constructor creates also a chunkRaw, preallocated if mode =
     * ChunkReaderMode.BUFFER
     *
     * @param clen
     * @param id
     * @param offsetInPng
     * Informational, is stored in chunkRaw
     * @param mode
     */
    init {
        if (mode == null || id.length != 4 || clen < 0) throw PngjInputException("Bad chunk paramenters: $mode")
        this.mode = mode
        chunkRaw = ChunkRaw(clen, id, mode == ChunkReaderMode.BUFFER)
        chunkRaw.offset = offsetInPng
        crcCheck = mode != ChunkReaderMode.SKIP // can be changed with setter
        // PngHelperInternal.debug("ChunkReader " + this.getClass() + " id="+id + " mode:"+mode);
    }

    /**
     * Consumes data for the chunk (data and CRC). This only consumes bytes
     * owned by this chunk (data + crc , not id+len prefix)
     *
     * In ChunkReaderMode.PROCESS can call processData() (not more than once)
     *
     * If this ends the chunk (included CRC) it checks CRC (if checking) and
     * calls chunkDone()
     *
     * @param buf
     * @param offset
     * @param len
     * @return How many bytes have been consumed
     */
    override fun consume(buf: ByteArray, offset: Int, len: Int): Int {
        var off = offset
        @Suppress("NAME_SHADOWING")
        var len = len
        if (len == 0) return 0
        if (len < 0) throw PngjException("negative length??")
        if (read == 0 && crcn == 0 && crcCheck) chunkRaw.updateCrc(
            chunkRaw.idbytes,
            0,
            4
        ) // initializes crc calculation with the Chunk ID
        var bytesForData = chunkRaw.len - read // bytesForData : bytes to be actually read from chunk data
        if (bytesForData > len) bytesForData = len
        // we want to call processData even for empty chunks (IEND:bytesForData=0) at
        // least once
        if (bytesForData > 0 || crcn == 0) {
            // in buffer mode we compute the CRC at the end
            if (crcCheck && mode != ChunkReaderMode.BUFFER && bytesForData > 0) chunkRaw.updateCrc(
                buf,
                off,
                bytesForData
            )
            if (mode == ChunkReaderMode.BUFFER) {
                // just copy the contents to the internal buffer
                if (!chunkRaw.data.contentEquals(buf) && bytesForData > 0) {
                    // If the buffer passed if the same as this one, we don't copy.
                    // The caller should know what he's doing
                    arraycopy(buf, off, chunkRaw.data!!, read, bytesForData)
                }
            } else if (mode == ChunkReaderMode.PROCESS) {
                processData(read, buf, off, bytesForData)
            } else {
                // mode == ChunkReaderMode.SKIP; nothing to do
            }
            read += bytesForData
            off += bytesForData
            len -= bytesForData
        }
        var crcRead = 0
        if (read == chunkRaw.len) { // data done - read crc?
            crcRead = 4 - crcn
            if (crcRead > len) crcRead = len
            if (crcRead > 0) {
                if (!buf.contentEquals(chunkRaw.crcval)) arraycopy(buf, off, chunkRaw.crcval, crcn, crcRead)
                crcn += crcRead
                if (crcn == 4) {
                    if (crcCheck) {
                        if (mode == ChunkReaderMode.BUFFER) { // in buffer mode we compute the CRC on one single call
                            chunkRaw.updateCrc(chunkRaw.data!!, 0, chunkRaw.len)
                        }
                        chunkRaw.checkCrc(errorBehav === ErrorBehaviour.STRICT)
                    }
                    println("Chunk done")
                    chunkDone()
                }
            }
        }
        return if (bytesForData > 0 || crcRead > 0) bytesForData + crcRead else -1 /* should not happen */
    }// has read all 4 bytes from the crc

    /**
     * Chunks has been read
     *
     * @return true if we have read all chunk, including trailing CRC
     */
    override val isDone: Boolean
        get() = crcn == 4 // has read all 4 bytes from the crc

    /**
     * Determines if CRC should be checked. This should be called before
     * starting reading.
     *
     * @see also .setErrorBehav
     * @param crcCheck
     */
    fun setCrcCheck(crcCheck: Boolean) {
        if (read != 0 && crcCheck && !this.crcCheck) throw PngjException("too late!")
        this.crcCheck = crcCheck
    }

    /**
     * This method will only be called in PROCESS mode, probably several times,
     * each time with a new fragment of data read from inside the chunk. For
     * chunks with zero-length data, this will still be called once.
     *
     * It's guaranteed that the data corresponds exclusively to this chunk data
     * (no crc, no data from no other chunks, )
     *
     * @param offsetInchunk
     * data bytes that had already been read/processed for this chunk
     * @param buf
     * @param off
     * @param len
     */
    protected abstract fun processData(offsetInchunk: Int, buf: ByteArray, off: Int, len: Int)

    /**
     * This method will be called (in all modes) when the full chunk -including
     * crc- has been read
     */
    protected abstract fun chunkDone()
    open val isFromDeflatedSet: Boolean
        get() = false

    override fun hashCode(): Int {
        val prime = 31
        var result = 1
        result = prime * result + chunkRaw.hashCode()
        return result
    }

    /**
     * Equality (and hash) is basically delegated to the ChunkRaw
     */
    override fun equals(other: Any?): Boolean { // delegates to chunkraw
        if (this === other) return true
        if (other == null) return false
        if (this::class != other::class) return false
        other as ChunkReader
        if (chunkRaw != other.chunkRaw) return false
        return true
    }

    override fun toString(): String {
        return chunkRaw.toString()
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy