1/* Copyright 2017 Google Inc. All Rights Reserved. 2 3 Distributed under MIT license. 4 See file LICENSE for detail or copy at https://opensource.org/licenses/MIT 5*/ 6 7package org.brotli.wrapper.enc; 8 9import java.io.IOException; 10import java.nio.ByteBuffer; 11import java.nio.channels.WritableByteChannel; 12import java.util.ArrayList; 13 14/** 15 * Base class for OutputStream / Channel implementations. 16 */ 17public class Encoder { 18 private final WritableByteChannel destination; 19 private final EncoderJNI.Wrapper encoder; 20 final ByteBuffer inputBuffer; 21 ByteBuffer buffer; 22 boolean closed; 23 24 /** 25 * Brotli encoder settings. 26 */ 27 public static final class Parameters { 28 private int quality = -1; 29 private int lgwin = -1; 30 31 public Parameters() { } 32 33 private Parameters(Parameters other) { 34 this.quality = other.quality; 35 this.lgwin = other.lgwin; 36 } 37 38 /** 39 * @param quality compression quality, or -1 for default 40 */ 41 public Parameters setQuality(int quality) { 42 if (quality < -1 || quality > 11) { 43 throw new IllegalArgumentException("quality should be in range [0, 11], or -1"); 44 } 45 this.quality = quality; 46 return this; 47 } 48 49 /** 50 * @param lgwin log2(LZ window size), or -1 for default 51 */ 52 public Parameters setWindow(int lgwin) { 53 if ((lgwin != -1) && ((lgwin < 10) || (lgwin > 24))) { 54 throw new IllegalArgumentException("lgwin should be in range [10, 24], or -1"); 55 } 56 this.lgwin = lgwin; 57 return this; 58 } 59 } 60 61 /** 62 * Creates a Encoder wrapper. 63 * 64 * @param destination underlying destination 65 * @param params encoding parameters 66 * @param inputBufferSize read buffer size 67 */ 68 Encoder(WritableByteChannel destination, Parameters params, int inputBufferSize) 69 throws IOException { 70 if (inputBufferSize <= 0) { 71 throw new IllegalArgumentException("buffer size must be positive"); 72 } 73 if (destination == null) { 74 throw new NullPointerException("destination can not be null"); 75 } 76 this.destination = destination; 77 this.encoder = new EncoderJNI.Wrapper(inputBufferSize, params.quality, params.lgwin); 78 this.inputBuffer = this.encoder.getInputBuffer(); 79 } 80 81 private void fail(String message) throws IOException { 82 try { 83 close(); 84 } catch (IOException ex) { 85 /* Ignore */ 86 } 87 throw new IOException(message); 88 } 89 90 /** 91 * @param force repeat pushing until all output is consumed 92 * @return true if all encoder output is consumed 93 */ 94 boolean pushOutput(boolean force) throws IOException { 95 while (buffer != null) { 96 if (buffer.hasRemaining()) { 97 destination.write(buffer); 98 } 99 if (!buffer.hasRemaining()) { 100 buffer = null; 101 } else if (!force) { 102 return false; 103 } 104 } 105 return true; 106 } 107 108 /** 109 * @return true if there is space in inputBuffer. 110 */ 111 boolean encode(EncoderJNI.Operation op) throws IOException { 112 boolean force = (op != EncoderJNI.Operation.PROCESS); 113 if (force) { 114 inputBuffer.limit(inputBuffer.position()); 115 } else if (inputBuffer.hasRemaining()) { 116 return true; 117 } 118 boolean hasInput = true; 119 while (true) { 120 if (!encoder.isSuccess()) { 121 fail("encoding failed"); 122 } else if (!pushOutput(force)) { 123 return false; 124 } else if (encoder.hasMoreOutput()) { 125 buffer = encoder.pull(); 126 } else if (encoder.hasRemainingInput()) { 127 encoder.push(op, 0); 128 } else if (hasInput) { 129 encoder.push(op, inputBuffer.limit()); 130 hasInput = false; 131 } else { 132 inputBuffer.clear(); 133 return true; 134 } 135 } 136 } 137 138 void flush() throws IOException { 139 encode(EncoderJNI.Operation.FLUSH); 140 } 141 142 void close() throws IOException { 143 if (closed) { 144 return; 145 } 146 closed = true; 147 try { 148 encode(EncoderJNI.Operation.FINISH); 149 } finally { 150 encoder.destroy(); 151 destination.close(); 152 } 153 } 154 155 /** 156 * Encodes the given data buffer. 157 */ 158 public static byte[] compress(byte[] data, Parameters params) throws IOException { 159 EncoderJNI.Wrapper encoder = new EncoderJNI.Wrapper(data.length, params.quality, params.lgwin); 160 ArrayList<byte[]> output = new ArrayList<byte[]>(); 161 int totalOutputSize = 0; 162 try { 163 encoder.getInputBuffer().put(data); 164 encoder.push(EncoderJNI.Operation.FINISH, data.length); 165 while (true) { 166 if (!encoder.isSuccess()) { 167 throw new IOException("encoding failed"); 168 } else if (encoder.hasMoreOutput()) { 169 ByteBuffer buffer = encoder.pull(); 170 byte[] chunk = new byte[buffer.remaining()]; 171 buffer.get(chunk); 172 output.add(chunk); 173 totalOutputSize += chunk.length; 174 } else if (!encoder.isFinished()) { 175 encoder.push(EncoderJNI.Operation.FINISH, 0); 176 } else { 177 break; 178 } 179 } 180 } finally { 181 encoder.destroy(); 182 } 183 if (output.size() == 1) { 184 return output.get(0); 185 } 186 byte[] result = new byte[totalOutputSize]; 187 int offset = 0; 188 for (byte[] chunk : output) { 189 System.arraycopy(chunk, 0, result, offset, chunk.length); 190 offset += chunk.length; 191 } 192 return result; 193 } 194 195 public static byte[] compress(byte[] data) throws IOException { 196 return compress(data, new Parameters()); 197 } 198} 199