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

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

There is a newer version: 4.8.0
Show 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("unused")
package org.jetbrains.letsPlot.util.pngj

import org.jetbrains.letsPlot.util.pngj.Zip.newInflater
import kotlin.jvm.JvmOverloads

/**
 * A set of IDAT-like chunks which, concatenated, form a zlib stream.
 *
 *
 * The inflated stream is intented to be read as a sequence of "rows", of which
 * the caller knows the lengths (not necessary equal!) and number.
 *
 *
 * Eg: For IDAT non-interlaced images, a row has bytesPerRow + 1 filter byte

* For interlaced images, the lengths are variable. * * * This class can work in sync (polled) mode or async (callback) mode. But for * callback mode the method processRowCallback() must be overriden * * * See [IdatSet], which is mostly used and has a slightly simpler use.

* See `DeflatedChunkSetTest` for example of use. */ internal open class DeflatedChunksSet @JvmOverloads constructor( /** * All IDAT-like chunks that form a same DeflatedChunksSet should have the * same id */ val chunkid: String, /** * Callback mode = async processing */ val isCallbackMode // true: CALLBACK (non-blocking) false: POLL (blocking) : Boolean, /** * Target size of the current row, including filter byte.

* should coincide (or be less than) with row.length */ var rowLen // what amount of bytes is to be interpreted as a complete "row". can change : Int, maxRowLen: Int, inflater: Inflater? = null, buffer: ByteArray? = null ) { var row // a "row" here means a raw (uncopressed filtered) part of the IDAT stream, : ByteArray? = null protected set // normally a image row (or subimage row for interlaced) plus a filter byte private val rowfilled // effective/valid length of row = 0 /** This the amount of valid bytes in the buffer */ // normally a image row (or subimage row for interlaced) plus a filter byte var rowFilled = 0 // effective/valid length of row private set private val rowlen = 0 // what amount of bytes is to be interpreted as a complete "row". can change /** * Get current (last) row number. * * * This corresponds to the raw numeration of rows as seen by the deflater. * Not the same as the real image row, if interlaced. * */ // (for interlaced) var rown: Int // only coincide with image row if non-interlaced - incremented by private set // setNextRowSize() /* * States WAITING_FOR_INPUT ROW_READY WORK_DONE TERMINATED * * processBytes() is externally called, prohibited in READY (in DONE it's * ignored) * * WARNING: inflater.finished() != DONE (not enough, not neccesary) DONE means * that we have already uncompressed all the data of interest. * * In non-callback mode, prepareForNextRow() is also externally called, in * * Flow: - processBytes() calls inflateData() * * inflateData() : if buffer is filled goes to READY * * else if ! inf.finished goes to WAITING * * else if any data goes to READY (incomplete data to be read) * * else goes to DONE * * in Callback mode, after going to READY, n=processCallback() is called and * then prepareForNextRow(n) is called. * * in Polled mode, prepareForNextRow(n) must be called from outside (after * checking state=READY) * * prepareForNextRow(n) goes to DONE if n==0 calls inflateData() again - end() * goes to DONE */ enum class State { WAITING_FOR_INPUT, // waiting for more bytes to be fed ROW_READY, // ready for consumption (might be less than fully filled), ephemeral state for // CALLBACK mode DONE, // all data of interest has been read, but we might accept still more trailing // chunks (we'll ignore them) CLOSED; // we are done, and also won't accept more IDAT chunks // the caller has already uncompressed all the data of interest or EOF val isDone: Boolean get() = this == DONE || this == CLOSED // we dont accept more chunks val isClosed: Boolean get() = this == CLOSED } var state = State.WAITING_FOR_INPUT // never null private var inf: Inflater private var infOwn // true if we own the inflater (we created it) = false private var curChunk: DeflatedChunkReader? = null /** total number of bytes that have been fed to this object */ var bytesIn: Long = 0 // count the total compressed bytes that have been fed private set /** total number of bytes that have been uncompressed */ var bytesOut: Long = 0 // count the total uncompressed bytes private set var chunkNum = -1 // incremented at each new chunk start var firstChunqSeqNum = -1 // expected seq num for first chunk. used only for fDAT (APNG) init { if (rowLen < 1 || maxRowLen < rowLen) throw PngjException("bad inital row len $rowLen") if (inflater != null) { inf = inflater infOwn = false } else { inf = newInflater() infOwn = true // inflater is own, we will release on close() } row = if (buffer != null && buffer.size >= rowLen) buffer else ByteArray(maxRowLen) rown = -1 state = State.WAITING_FOR_INPUT try { prepareForNextRow(rowLen) } catch (e: Exception) { close() throw e } } fun appendNewChunk(cr: DeflatedChunkReader) { // all chunks must have same id if (chunkid != cr.chunkRaw.id) throw PngjInputException( "Bad chunk inside IdatSet, id:" + cr.chunkRaw.id + ", expected:" + chunkid ) curChunk = cr chunkNum++ if (firstChunqSeqNum >= 0) cr.setSeqNumExpected(chunkNum + firstChunqSeqNum) } /** * Feeds the inflater with the compressed bytes * * In poll mode, the caller should not call repeatedly this, without * consuming first, checking isDataReadyForConsumer() * * @param buf * @param off * @param len */ fun processBytes(buf: ByteArray, off: Int, len: Int) { bytesIn += len.toLong() // PngHelperInternal.LOGGER.info("processing compressed bytes in chunkreader : " // + len); if (len < 1 || state.isDone) return if (state == State.ROW_READY) throw PngjInputException("this should only be called if waitingForMoreInput") if (inf.needsDictionary || !inf.needsInput) error("should not happen") inf.setInput(buf, off, len) // PngHelperInternal.debug("entering processs bytes, state=" + state + // " callback="+callbackMode); if (isCallbackMode) { while (inflateData()) { val nextRowLen = processRowCallback() prepareForNextRow(nextRowLen) if (isDone) processDoneCallback() } } else inflateData() } /* * This never inflates more than one row This returns true if this has resulted * in a row being ready and preprocessed with preProcessRow (in callback mode, * we should call immediately processRowCallback() and * prepareForNextRow(nextRowLen) */ private fun inflateData(): Boolean { try { // PngHelperInternal.debug("entering inflateData bytes, state=" + state + // " callback="+callbackMode); if (state == State.ROW_READY) throw PngjException("invalid state") // assert if (state.isDone) return false val ninflated: Int if (row == null || row!!.size < rowLen) row = ByteArray(rowLen) // should not happen if (rowFilled < rowLen && !inf.finished) { ninflated = inf.inflate(row!!, rowFilled, rowLen - rowFilled) rowFilled += ninflated bytesOut += ninflated.toLong() } val nextstate: State = if (rowFilled == rowLen) State.ROW_READY // complete row, process it else if (!inf.finished) State.WAITING_FOR_INPUT else if (rowFilled > 0) State.ROW_READY // complete row, process it else { State.DONE // eof, no more data } state = nextstate if (state == State.ROW_READY) { preProcessRow() return true } } catch (e: Exception) { close() throw e } return false } /** * Called automatically in all modes when a full row has been fully * inflated. */ protected open fun preProcessRow() {} /** * Callback, must be implemented in callbackMode * * * This should use [.getRowFilled] and [.getInflatedRow] to * access the row. * * * Must return byes of next row, for next callback. */ protected open fun processRowCallback(): Int { throw PngjInputException("not implemented") } /** * Callback, to be implemented in callbackMode * * * This will be called once to notify state done */ protected open fun processDoneCallback() {} /** * Should be called after the previous row was processed * * * Pass 0 or negative to signal that we are done (not expecting more bytes) * * * This resets [.rowfilled] * * * The */ fun prepareForNextRow(len: Int) { rowFilled = 0 rown++ if (len < 1) { rowLen = 0 markAsDone() } else if (inf.finished) { rowLen = 0 markAsDone() } else { state = State.WAITING_FOR_INPUT rowLen = len if (!isCallbackMode) inflateData() } } /** * In this state, the object is waiting for more input to deflate. * * * Only in this state it's legal to feed this */ val isWaitingForMoreInput: Boolean get() = state == State.WAITING_FOR_INPUT /** * In this state, the object is waiting the caller to retrieve inflated data * * * Effective length: see [.getRowFilled] */ open val isRowReady: Boolean get() = state == State.ROW_READY /** * In this state, all relevant data has been uncompressed and retrieved * (exceptionally, the reading has ended prematurely). * * * We can still feed this object, but the bytes will be swallowed/ignored. */ val isDone: Boolean get() = state.isDone val isClosed: Boolean get() = state.isClosed /** * This will be called by the owner to report us the next chunk to come. We * can make our own internal changes and checks. This returns true if we * acknowledge the next chunk as part of this set */ fun ackNextChunkId(id: String): Boolean { return if (state.isClosed) false else if (id == chunkid) { true } else { if (!allowOtherChunksInBetween(id)) { if (state.isDone) { if (!state.isClosed) close() false } else { throw PngjInputException("Unexpected chunk $id while $chunkid set is not done") } } else true } } /** * This should be called when discarding this object, or for aborting. * Secure, idempotent Don't use this just to notify this object that it has * no more work to do, see [.markAsDone] */ open fun close() { try { if (!state.isClosed) { state = State.CLOSED } if (infOwn) { inf.end() // we end the Inflater only if we created it } } catch (_: Exception) { } } /** * Forces the DONE state (except if it was CLOSED), this object won't uncompress more data. It's still * not terminated, it will accept more IDAT chunks, but will ignore them. */ fun markAsDone() { if (!isDone) state = State.DONE } /** * Some IDAT-like set can allow other chunks in between (APGN?). * * * Normally false. * * @param id * Id of the other chunk that appeared in middel of this set. * @return true if allowed */ @Suppress("UNUSED_PARAMETER") fun allowOtherChunksInBetween(id: String?): Boolean { return false } override fun toString(): String { val sb = StringBuilder( "idatSet : " + curChunk!!.chunkRaw.id + " state=" + state + " rows=" + rown + " bytes=" + bytesIn + "/" + bytesOut ) return sb.toString() } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy