Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(631)

Side by Side Diff: src/pkg/image/jpeg/writer.go

Issue 4435051: code review 4435051: image/jpeg: add an encoder. (Closed)
Patch Set: diff -r 6994a5f47fb9 https://go.googlecode.com/hg/ Created 13 years, 11 months ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « src/pkg/image/jpeg/reader.go ('k') | src/pkg/image/jpeg/writer_test.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2011 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4
5 package jpeg
6
7 import (
8 "bufio"
9 "image"
10 "image/ycbcr"
11 "io"
12 "os"
13 )
14
15 // min returns the minimum of two integers.
16 func min(x, y int) int {
17 if x < y {
18 return x
19 }
20 return y
21 }
22
23 // div returns a/b rounded to the nearest integer, instead of rounded to zero.
24 func div(a int, b int) int {
25 if a >= 0 {
26 return (a + (b >> 1)) / b
27 }
28 return -((-a + (b >> 1)) / b)
29 }
30
31 // bitCount counts the number of bits needed to hold an integer.
32 var bitCount = [256]byte{
33 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4,
34 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
35 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
36 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
37 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
38 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
39 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
40 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
41 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
42 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
43 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
44 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
45 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
46 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
47 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
48 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
49 }
50
51 type quantIndex int
52
53 const (
54 quantIndexLuminance quantIndex = iota
55 quantIndexChrominance
56 nQuantIndex
57 )
58
59 // unscaledQuant are the unscaled quantization tables. Each encoder copies and
60 // scales the tables according to its quality parameter.
61 var unscaledQuant = [nQuantIndex][blockSize]byte{
62 // Luminance.
63 {
64 16, 11, 10, 16, 24, 40, 51, 61,
65 12, 12, 14, 19, 26, 58, 60, 55,
66 14, 13, 16, 24, 40, 57, 69, 56,
67 14, 17, 22, 29, 51, 87, 80, 62,
68 18, 22, 37, 56, 68, 109, 103, 77,
69 24, 35, 55, 64, 81, 104, 113, 92,
70 49, 64, 78, 87, 103, 121, 120, 101,
71 72, 92, 95, 98, 112, 100, 103, 99,
72 },
73 // Chrominance.
74 {
75 17, 18, 24, 47, 99, 99, 99, 99,
76 18, 21, 26, 66, 99, 99, 99, 99,
77 24, 26, 56, 99, 99, 99, 99, 99,
78 47, 66, 99, 99, 99, 99, 99, 99,
79 99, 99, 99, 99, 99, 99, 99, 99,
80 99, 99, 99, 99, 99, 99, 99, 99,
81 99, 99, 99, 99, 99, 99, 99, 99,
82 99, 99, 99, 99, 99, 99, 99, 99,
83 },
84 }
85
86 type huffIndex int
87
88 const (
89 huffIndexLuminanceDC huffIndex = iota
90 huffIndexLuminanceAC
91 huffIndexChrominanceDC
92 huffIndexChrominanceAC
93 nHuffIndex
94 )
95
96 // huffmanSpec specifies a Huffman encoding.
97 type huffmanSpec struct {
98 // count[i] is the number of codes of length i bits.
99 count [16]byte
100 // value[i] is the decoded value of the i'th codeword.
101 value []byte
102 }
103
104 // theHuffmanSpec is the Huffman encoding specifications.
105 // This encoder uses the same Huffman encoding for all images.
106 var theHuffmanSpec = [nHuffIndex]huffmanSpec{
107 // Luminance DC.
108 {
109 [16]byte{0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0},
110 []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
111 },
112 // Luminance AC.
113 {
114 [16]byte{0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 125},
115 []byte{
116 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12,
117 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
118 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08,
119 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0,
120 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16,
121 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28,
122 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
123 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
124 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
125 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
126 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
127 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
128 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
129 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
130 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6,
131 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5,
132 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4,
133 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2,
134 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
135 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
136 0xf9, 0xfa,
137 },
138 },
139 // Chrominance DC.
140 {
141 [16]byte{0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0},
142 []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
143 },
144 // Chrominance AC.
145 {
146 [16]byte{0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 119},
147 []byte{
148 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21,
149 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71,
150 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91,
151 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0,
152 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34,
153 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26,
154 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38,
155 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
156 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
157 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
158 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,
159 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
160 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96,
161 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5,
162 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4,
163 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3,
164 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2,
165 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda,
166 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9,
167 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
168 0xf9, 0xfa,
169 },
170 },
171 }
172
173 // huffmanLUT is a compiled look-up table representation of a huffmanSpec.
174 // Each value maps to an uint32 of which the 8 most significant bits hold the
r 2011/04/18 19:30:42 s/an/a/
nigeltao 2011/04/19 00:47:06 Done.
175 // codeword size in bits and the 24 least significant bits hold the codeword.
176 // The maximum codeword size is 16 bits.
177 type huffmanLUT []uint32
178
179 func (h *huffmanLUT) init(s huffmanSpec) {
180 maxValue := 0
181 for _, v := range s.value {
182 if int(v) > maxValue {
183 maxValue = int(v)
184 }
185 }
186 *h = make([]uint32, maxValue+1)
187 code, k := uint32(0), 0
188 for i := 0; i < len(s.count); i++ {
189 nBits := uint32(i+1) << 24
190 for j := uint8(0); j < s.count[i]; j++ {
191 (*h)[s.value[k]] = nBits | code
192 code++
193 k++
194 }
195 code <<= 1
196 }
197 }
198
199 // theHuffmanLUT are compiled representations of theHuffmanSpec.
200 var theHuffmanLUT [4]huffmanLUT
201
202 func init() {
203 for i, s := range theHuffmanSpec {
204 theHuffmanLUT[i].init(s)
205 }
206 }
207
208 // writer is a buffered writer.
209 type writer interface {
210 Flush() os.Error
211 Write([]byte) (int, os.Error)
212 WriteByte(byte) os.Error
213 WriteString(string) (int, os.Error)
214 }
215
216 // An encoder wraps a writer's methods to save the first returned error as
217 // e.err and return void.
r 2011/04/18 19:30:42 void?
nigeltao 2011/04/19 00:47:06 Rewritten.
218
219 func (e *encoder) flush() {
220 if e.err != nil {
221 return
222 }
223 e.err = e.w.Flush()
224 }
225
226 func (e *encoder) write(p []byte) {
227 if e.err != nil {
228 return
229 }
230 _, e.err = e.w.Write(p)
231 }
232
233 func (e *encoder) writeByte(b byte) {
234 if e.err != nil {
235 return
236 }
237 e.err = e.w.WriteByte(b)
238 }
239
240 func (e *encoder) writeString(s string) {
241 if e.err != nil {
242 return
243 }
244 _, e.err = e.w.WriteString(s)
245 }
246
247 // encoder encodes an image to the JPEG format.
r 2011/04/18 19:30:42 move this above its methods and combine the commen
nigeltao 2011/04/19 00:47:06 Done.
248 type encoder struct {
249 // w is the writer to write to. err is the first error encountered
250 // during writing.
251 w writer
252 err os.Error
253 // bits and nBits are accumulated bits to write to w.
254 bits uint32
255 nBits uint8
256 // quant is the scaled quantization tables.
257 quant [nQuantIndex][blockSize]byte
258 }
259
260 // emit emits the least significant nBits bits of bits to the bitstream.
r 2011/04/18 19:30:42 nBits bits of bits to the bits. wow.
261 // The precondition is bits < 1<<nBits && nBits <= 16.
262 func (e *encoder) emit(bits uint32, nBits uint8) {
263 nBits += e.nBits
264 bits <<= 32 - nBits
265 bits |= e.bits
266 for nBits >= 8 {
267 b := uint8(bits >> 24)
268 e.writeByte(b)
269 if b == 255 {
270 e.writeByte(0x00)
271 }
272 bits <<= 8
273 nBits -= 8
274 }
275 e.bits, e.nBits = bits, nBits
276 }
277
278 // emitHuff emits the given value with the given Huffman encoder.
279 func (e *encoder) emitHuff(h huffIndex, value int) {
280 x := theHuffmanLUT[h][value]
281 e.emit(x&(1<<24-1), uint8(x>>24))
282 }
283
284 // emitHuffRLE emits a run of runLength copies of value encoded with the given
285 // Huffman encoder.
286 func (e *encoder) emitHuffRLE(h huffIndex, runLength, value int) {
287 a, b := value, value
288 if a < 0 {
289 a, b = -value, value-1
290 }
291 var nBits uint8
292 if a < 0x100 {
293 nBits = bitCount[a]
294 } else {
295 nBits = 8 + bitCount[a>>8]
296 }
297 e.emitHuff(h, runLength<<4|int(nBits))
298 if nBits > 0 {
299 e.emit(uint32(b)&(1<<nBits-1), nBits)
300 }
301 }
302
303 // writeMarkerHeader writes the header for a maker with the given length.
r 2011/04/18 19:30:42 s/maker/marker/
nigeltao 2011/04/19 00:47:06 Done.
304 func (e *encoder) writeMarkerHeader(marker uint8, markerlen int) {
305 e.writeByte(0xff)
306 e.writeByte(marker)
307 e.writeByte(uint8(markerlen >> 8))
308 e.writeByte(uint8(markerlen & 0xff))
r 2011/04/18 19:30:42 this is a fair bit of overhead. we could put a [4]
nigeltao 2011/04/19 00:47:06 Done.
309 }
310
311 // writeDQT writes the Define Quantization Table marker.
312 func (e *encoder) writeDQT() {
313 markerlen := 2
314 for _, q := range e.quant {
315 markerlen += 1 + len(q)
316 }
317 e.writeMarkerHeader(dqtMarker, markerlen)
318 for i, q := range e.quant {
319 e.writeByte(uint8(i))
320 e.write(q[:])
321 }
322 }
323
324 // writeSOF0 writes the Start Of Frame (Baseline) marker.
325 func (e *encoder) writeSOF0(size image.Point) {
326 markerlen := 8 + 3*nComponent
327 e.writeMarkerHeader(sof0Marker, markerlen)
328 e.writeByte(8) // 8-bit color.
329 e.writeByte(uint8(size.Y >> 8))
330 e.writeByte(uint8(size.Y & 0xff))
331 e.writeByte(uint8(size.X >> 8))
332 e.writeByte(uint8(size.X & 0xff))
r 2011/04/18 19:30:42 ditto.
nigeltao 2011/04/19 00:47:06 Done.
333 e.writeByte(nComponent)
334 for i := 0; i < nComponent; i++ {
335 e.writeByte(uint8(i + 1))
336 // We use 4:2:0 chroma subsampling.
337 e.writeByte("\x22\x11\x11"[i])
338 e.writeByte("\x00\x01\x01"[i])
r 2011/04/18 19:30:42 ditto
nigeltao 2011/04/19 00:47:06 Done.
339 }
340 }
341
342 // writeDHT writes the Define Huffman Table marker.
343 func (e *encoder) writeDHT() {
344 markerlen := 2
345 for _, s := range theHuffmanSpec {
346 markerlen += 1 + 16 + len(s.value)
347 }
348 e.writeMarkerHeader(dhtMarker, markerlen)
349 for i, s := range theHuffmanSpec {
350 e.writeByte("\x00\x10\x01\x11"[i])
351 e.write(s.count[:])
352 e.write(s.value)
r 2011/04/18 19:30:42 ditto
nigeltao 2011/04/19 00:47:06 I don't think this applies here; s.value is a slic
353 }
354 }
355
356 // writeBlock writes a block of pixel data using the given quantization table,
357 // returning the post-quantized DC value of the DCT-transformed block.
358 func (e *encoder) writeBlock(b *block, q quantIndex, prevDC int) int {
359 fdct(b)
360 // Emit the DC delta.
361 dc := div(b[0], (8 * int(e.quant[q][0])))
362 e.emitHuffRLE(huffIndex(2*q+0), 0, dc-prevDC)
363 // Emit the AC components.
364 h, runLength := huffIndex(2*q+1), 0
365 for k := 1; k < blockSize; k++ {
366 ac := div(b[unzig[k]], (8 * int(e.quant[q][k])))
367 if ac == 0 {
368 runLength++
369 } else {
370 for runLength > 15 {
371 e.emitHuff(h, 0xf0)
372 runLength -= 16
373 }
374 e.emitHuffRLE(h, runLength, ac)
375 runLength = 0
376 }
377 }
378 if runLength > 0 {
379 e.emitHuff(h, 0x00)
380 }
381 return dc
382 }
383
384 // toYCbCr converts the 8x8 region of m whose top-left corner is p to its
385 // YCbCr values.
386 func toYCbCr(m image.Image, p image.Point, yBlock, cbBlock, crBlock *block) {
387 b := m.Bounds()
388 xmax := b.Max.X - 1
389 ymax := b.Max.Y - 1
390 for j := 0; j < 8; j++ {
391 for i := 0; i < 8; i++ {
392 r, g, b, _ := m.At(min(p.X+i, xmax), min(p.Y+j, ymax)).R GBA()
393 yy, cb, cr := ycbcr.RGBToYCbCr(uint8(r>>8), uint8(g>>8), uint8(b>>8))
394 yBlock[8*j+i] = int(yy)
395 cbBlock[8*j+i] = int(cb)
396 crBlock[8*j+i] = int(cr)
397 }
398 }
399 }
400
401 // scale scales the 16x16 region represented by the 4 src blocks to the 8x8
402 // dst block.
403 func scale(dst *block, src *[4]block) {
404 for i := 0; i < 4; i++ {
405 dstOff := (i&2)<<4 | (i&1)<<2
406 for y := 0; y < 4; y++ {
407 for x := 0; x < 4; x++ {
408 j := 16*y + 2*x
409 sum := src[i][j] + src[i][j+1] + src[i][j+8] + s rc[i][j+9]
410 dst[8*y+x+dstOff] = (sum + 2) >> 2
411 }
412 }
413 }
414 }
415
416 // writeSOS writes the StartOfScan marker.
417 func (e *encoder) writeSOS(m image.Image) {
418 // Write the SOS marker "\xff\xda" followed by 12 bytes:
419 // - the marker length "\x00\x0c",
420 // - the number of components "\x03",
421 // - component 1 uses DC table 0 and AC table 0 "\x01\x00",
422 // - component 2 uses DC table 1 and AC table 1 "\x02\x11",
423 // - component 3 uses DC table 1 and AC table 1 "\x03\x11",
424 // - padding "\x00\x00\x00".
425 e.writeString("\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00\x00\x00" )
r 2011/04/18 19:30:42 depending on the implementation of the writer, wri
nigeltao 2011/04/19 00:47:06 Done.
426 var (
427 // Scratch buffers to hold the YCbCr values.
428 yBlock block
429 cbBlock [4]block
430 crBlock [4]block
431 cBlock block
r 2011/04/18 19:30:42 ok for now, but these are more unnecessary allocat
nigeltao 2011/04/19 00:47:06 There is only one SOS per image. Also, eventually,
432 // DC components are delta-encoded.
433 lastY, lastCb, lastCr int
434 )
435 bounds := m.Bounds()
436 for y := bounds.Min.Y; y < bounds.Max.Y; y += 16 {
437 for x := bounds.Min.X; x < bounds.Max.X; x += 16 {
438 for i := 0; i < 4; i++ {
439 xOff := (i & 1) * 8
440 yOff := (i & 2) * 4
441 p := image.Point{x + xOff, y + yOff}
442 toYCbCr(m, p, &yBlock, &cbBlock[i], &crBlock[i])
443 lastY = e.writeBlock(&yBlock, 0, lastY)
444 }
445 scale(&cBlock, &cbBlock)
446 lastCb = e.writeBlock(&cBlock, 1, lastCb)
447 scale(&cBlock, &crBlock)
448 lastCr = e.writeBlock(&cBlock, 1, lastCr)
449 }
450 }
451 // Pad the last byte with 1's.
452 e.emit(0x7f, 7)
453 }
454
455 // DefaultQuality is the default quality encoding parameter.
456 const DefaultQuality = 75
457
458 // Options are the encoding parameters.
459 // Quality ranges from 1 to 100 inclusive, higher is better.
460 type Options struct {
461 Quality int
462 }
463
464 // Encode writes the Image m to w in JPEG 4:2:0 baseline format with the given
465 // options. Default parameters are used if a nil *Options is passed.
466 func Encode(w io.Writer, m image.Image, o *Options) os.Error {
467 b := m.Bounds()
468 if b.Dx() >= 1<<16 || b.Dy() >= 1<<16 {
469 return os.NewError("jpeg: image is too large to encode")
470 }
471 var e encoder
472 if ww, ok := w.(writer); ok {
473 e.w = ww
474 } else {
475 e.w = bufio.NewWriter(w)
476 }
477 // Clip quality to [1, 100].
478 quality := DefaultQuality
479 if o != nil {
480 quality = o.Quality
481 if quality < 1 {
482 quality = 1
483 } else if quality > 100 {
484 quality = 100
485 }
486 }
487 // Convert from a quality rating to a scaling factor.
488 var scale int
489 if quality < 50 {
490 scale = 5000 / quality
491 } else {
492 scale = 200 - quality*2
493 }
494 // Initialize the quantization tables.
495 for i := range e.quant {
496 for j := range e.quant[i] {
497 x := int(unscaledQuant[i][j])
498 x = (x*scale + 50) / 100
499 if x < 1 {
500 x = 1
501 } else if x > 255 {
502 x = 255
503 }
504 e.quant[i][j] = uint8(x)
505 }
506 }
507 // Write the Start Of Image marker.
508 e.writeString("\xff\xd8")
509 // Write the quantization tables.
510 e.writeDQT()
511 // Write the image dimensions.
512 e.writeSOF0(b.Size())
513 // Write the Huffman tables.
514 e.writeDHT()
515 // Write the image data.
516 e.writeSOS(m)
517 // Write the End Of Image marker.
518 e.writeString("\xff\xd9")
519 e.flush()
520 return e.err
521 }
OLDNEW
« no previous file with comments | « src/pkg/image/jpeg/reader.go ('k') | src/pkg/image/jpeg/writer_test.go » ('j') | no next file with comments »

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld f62528b