Skip to content

Commit

Permalink
Merge pull request google#158 from davidsansome/cgo
Browse files Browse the repository at this point in the history
Add a CGO wrapper
  • Loading branch information
lvandeve authored Jan 18, 2019
2 parents 1ca477e + e56f4dc commit ef109dd
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 0 deletions.
58 changes: 58 additions & 0 deletions go/zopfli/zopfli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package zopfli provides a simple Go interface for Zopfli compression.
package zopfli

/*
#cgo LDFLAGS: -lzopfli -lm
#include <limits.h> // for INT_MAX
#include <stdlib.h> // for free()
#include <string.h> // for memmove()
#include "zopfli.h"
*/
import "C"
import "unsafe"

// Zopfli can't handle empty input, so we use a static result.
const emptyGzip = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00"

// Gzip compresses data with Zopfli using default settings and gzip format.
// The Zopfli library does not return errors, and there are no (detectable)
// failure cases, hence no error return.
func Gzip(inputSlice []byte) []byte {
var options C.struct_ZopfliOptions
C.ZopfliInitOptions(&options)

inputSize := (C.size_t)(len(inputSlice))
if inputSize == 0 {
return []byte(emptyGzip)
}
input := (*C.uchar)(unsafe.Pointer(&inputSlice[0]))
var compressed *C.uchar
var compressedLength C.size_t

C.ZopfliCompress(&options, C.ZOPFLI_FORMAT_GZIP,
input, inputSize,
&compressed, &compressedLength)
defer C.free(unsafe.Pointer(compressed))

// GoBytes only accepts int, not C.size_t. The code below does the same minus
// protection against zero-length values, but compressedLength is never 0 due
// to headers.
result := make([]byte, compressedLength)
C.memmove(unsafe.Pointer(&result[0]), unsafe.Pointer(compressed),
compressedLength)
return result
}
69 changes: 69 additions & 0 deletions go/zopfli/zopfli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package zopfli

import (
"bytes"
"compress/gzip"
"io/ioutil"
"math/rand"
"strings"
"testing"
)

func getRandomBytes(length uint64) []byte {
rng := rand.New(rand.NewSource(1)) // Make test repeatable.
data := make([]byte, length)
for i := uint64(0); i < length; i++ {
data[i] = (byte)(rng.Int())
}
return data
}

// TestGzip verifies that Gzip compresses data correctly.
func TestGzip(t *testing.T) {
compressibleString := "compressthis" + strings.Repeat("_foobar", 1000) + "$"

for _, test := range []struct {
name string
data []byte
maxSize int
}{
{"compressible string", []byte(compressibleString), 500},
{"random binary data", getRandomBytes(3000), 3100},
{"empty string", []byte(""), 20},
} {
compressed := Gzip(test.data)
gzipReader, err := gzip.NewReader(bytes.NewReader(compressed))
if err != nil {
t.Errorf("%s: gzip.NewReader: got error %v, expected no error",
test.name, err)
continue
}
decompressed, err := ioutil.ReadAll(gzipReader)
if err != nil {
t.Errorf("%s: reading gzip stream: got error %v, expected no error",
test.name, err)
continue
}
if bytes.Compare(test.data, decompressed) != 0 {
t.Errorf("%s: mismatch between input and decompressed data", test.name)
continue
}
if test.maxSize > 0 && len(compressed) > test.maxSize {
t.Errorf("%s: compressed data is %d bytes, expected %d or less",
test.name, len(compressed), test.maxSize)
}
}
}
Binary file added go/zopflipng/testdata/zoidberg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 86 additions & 0 deletions go/zopflipng/zopflipng.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package zopflipng

import (
"fmt"
)

/*
#cgo LDFLAGS: -lzopflipng -lzopfli -lstdc++ -lm
#include <stdlib.h>
#include <string.h>
#include "zopflipng_lib.h"
*/
import "C"
import "unsafe"

// Options allows overriding of some internal parameters.
type Options struct {
LossyTransparent bool
Lossy8bit bool
NumIterations int
NumIterationsLarge int
}

// NewOptions creates an options struct with the default parameters.
func NewOptions() *Options {
ret := &Options{
LossyTransparent: false,
Lossy8bit: false,
NumIterations: 15,
NumIterationsLarge: 5,
}
return ret
}

// Compress recompresses a PNG using Zopfli.
func Compress(inputSlice []byte) ([]byte, error) {
return CompressWithOptions(inputSlice, NewOptions())
}

// CompressWithOptions allows overriding some internal parameters.
func CompressWithOptions(inputSlice []byte, options *Options) ([]byte, error) {
cOptions := createCOptions(options)
input := (*C.uchar)(unsafe.Pointer(&inputSlice[0]))
inputSize := (C.size_t)(len(inputSlice))
var compressed *C.uchar
var compressedLength C.size_t
errCode := int(C.CZopfliPNGOptimize(input, inputSize, &cOptions, 0, &compressed, &compressedLength))
defer C.free(unsafe.Pointer(compressed))
if errCode != 0 {
return nil, fmt.Errorf("ZopfliPng failed with code: %d", errCode)
}

result := make([]byte, compressedLength)
C.memmove(unsafe.Pointer(&result[0]), unsafe.Pointer(compressed), compressedLength)
return result, nil
}

func createCOptions(options *Options) C.struct_CZopfliPNGOptions {
var cOptions C.struct_CZopfliPNGOptions
C.CZopfliPNGSetDefaults(&cOptions)
cOptions.lossy_transparent = boolToInt(options.LossyTransparent)
cOptions.lossy_8bit = boolToInt(options.Lossy8bit)
cOptions.num_iterations = C.int(options.NumIterations)
cOptions.num_iterations_large = C.int(options.NumIterationsLarge)
return cOptions
}

func boolToInt(b bool) C.int {
if b {
return C.int(1)
}
return C.int(0)
}
35 changes: 35 additions & 0 deletions go/zopflipng/zopflipng_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package zopflipng

import (
"io/ioutil"
"testing"
)

// TestCompress verifies that ZopfliPng compresses PNGs correctly.
func TestCompress(t *testing.T) {
path := "testdata/zoidberg.png"
contents, err := ioutil.ReadFile(path)
if err != nil {
t.Errorf("Failed to load testdata: %s", path)
}
compressed, err := Compress(contents)
if err != nil {
t.Error("ZopfliPNG failed: ", err)
}
if len(compressed) >= len(contents) {
t.Error("ZopfliPNG did not compress png")
}
}

0 comments on commit ef109dd

Please sign in to comment.