/* Copyright 2017 Google Inc. All Rights Reserved. Distributed under MIT license. See file LICENSE for detail or copy at https://opensource.org/licenses/MIT */ package org.brotli.wrapper.enc; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.WritableByteChannel; import java.util.ArrayList; /** * Base class for OutputStream / Channel implementations. */ public class Encoder { private final WritableByteChannel destination; private final EncoderJNI.Wrapper encoder; final ByteBuffer inputBuffer; ByteBuffer buffer; boolean closed; /** * Brotli encoder settings. */ public static final class Parameters { private int quality = -1; private int lgwin = -1; public Parameters() { } private Parameters(Parameters other) { this.quality = other.quality; this.lgwin = other.lgwin; } /** * @param quality compression quality, or -1 for default */ public Parameters setQuality(int quality) { if (quality < -1 || quality > 11) { throw new IllegalArgumentException("quality should be in range [0, 11], or -1"); } this.quality = quality; return this; } /** * @param lgwin log2(LZ window size), or -1 for default */ public Parameters setWindow(int lgwin) { if ((lgwin != -1) && ((lgwin < 10) || (lgwin > 24))) { throw new IllegalArgumentException("lgwin should be in range [10, 24], or -1"); } this.lgwin = lgwin; return this; } } /** * Creates a Encoder wrapper. * * @param destination underlying destination * @param params encoding parameters * @param inputBufferSize read buffer size */ Encoder(WritableByteChannel destination, Parameters params, int inputBufferSize) throws IOException { if (inputBufferSize <= 0) { throw new IllegalArgumentException("buffer size must be positive"); } if (destination == null) { throw new NullPointerException("destination can not be null"); } this.destination = destination; this.encoder = new EncoderJNI.Wrapper(inputBufferSize, params.quality, params.lgwin); this.inputBuffer = this.encoder.getInputBuffer(); } private void fail(String message) throws IOException { try { close(); } catch (IOException ex) { /* Ignore */ } throw new IOException(message); } /** * @param force repeat pushing until all output is consumed * @return true if all encoder output is consumed */ boolean pushOutput(boolean force) throws IOException { while (buffer != null) { if (buffer.hasRemaining()) { destination.write(buffer); } if (!buffer.hasRemaining()) { buffer = null; } else if (!force) { return false; } } return true; } /** * @return true if there is space in inputBuffer. */ boolean encode(EncoderJNI.Operation op) throws IOException { boolean force = (op != EncoderJNI.Operation.PROCESS); if (force) { inputBuffer.limit(inputBuffer.position()); } else if (inputBuffer.hasRemaining()) { return true; } boolean hasInput = true; while (true) { if (!encoder.isSuccess()) { fail("encoding failed"); } else if (!pushOutput(force)) { return false; } else if (encoder.hasMoreOutput()) { buffer = encoder.pull(); } else if (encoder.hasRemainingInput()) { encoder.push(op, 0); } else if (hasInput) { encoder.push(op, inputBuffer.limit()); hasInput = false; } else { inputBuffer.clear(); return true; } } } void flush() throws IOException { encode(EncoderJNI.Operation.FLUSH); } void close() throws IOException { if (closed) { return; } closed = true; try { encode(EncoderJNI.Operation.FINISH); } finally { encoder.destroy(); destination.close(); } } /** * Encodes the given data buffer. */ public static byte[] compress(byte[] data, Parameters params) throws IOException { EncoderJNI.Wrapper encoder = new EncoderJNI.Wrapper(data.length, params.quality, params.lgwin); ArrayList output = new ArrayList(); int totalOutputSize = 0; try { encoder.getInputBuffer().put(data); encoder.push(EncoderJNI.Operation.FINISH, data.length); while (true) { if (!encoder.isSuccess()) { throw new IOException("encoding failed"); } else if (encoder.hasMoreOutput()) { ByteBuffer buffer = encoder.pull(); byte[] chunk = new byte[buffer.remaining()]; buffer.get(chunk); output.add(chunk); totalOutputSize += chunk.length; } else if (!encoder.isFinished()) { encoder.push(EncoderJNI.Operation.FINISH, 0); } else { break; } } } finally { encoder.destroy(); } if (output.size() == 1) { return output.get(0); } byte[] result = new byte[totalOutputSize]; int offset = 0; for (byte[] chunk : output) { System.arraycopy(chunk, 0, result, offset, chunk.length); offset += chunk.length; } return result; } public static byte[] compress(byte[] data) throws IOException { return compress(data, new Parameters()); } }