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