aboutsummaryrefslogtreecommitdiff
path: root/java/org/brotli/wrapper/enc/Encoder.java
blob: f35bc2277f83c15a313d7ddd24906e3153a92e97 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/* 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<byte[]> output = new ArrayList<byte[]>();
    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());
  }
}