Skip to content

feat: VipsTarget integration with io.WriteCloser #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# vipsgen

[![CI](https://github.com/cshum/vipsgen/actions/workflows/ci.yml/badge.svg)](https://github.com/cshum/vipsgen/actions/workflows/ci.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/cshum/vipsgen/vips.svg)](https://pkg.go.dev/github.com/cshum/vipsgen/vips)
[![CI](https://github.com/cshum/vipsgen/actions/workflows/ci.yml/badge.svg)](https://github.com/cshum/vipsgen/actions/workflows/ci.yml)
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/cshum/vipsgen)](https://github.com/cshum/vipsgen/releases)

vipsgen is a Go binding generator for [libvips](https://www.libvips.org/) - a fast and efficient image processing library.
vipsgen is a Go binding generator for [libvips](https://github.com/libvips/libvips) - a fast and efficient image processing library.

Existing Go libvips bindings rely on manually written code that is often incomplete, error-prone, and difficult to maintain as libvips evolves.
vipsgen solves this by generating type-safe, robust, and fully documented Go bindings using GObject introspection.
Expand All @@ -19,7 +19,7 @@ You can use vipsgen in two ways:
- **Comprehensive**: Bindings for around [300 libvips operations](https://www.libvips.org/API/current/func-list.html)
- **Type-Safe**: Proper Go types for all libvips C enums and structs
- **Idiomatic**: Clean Go APIs that feel natural to use
- **Streaming**: Includes `VipsSource` integration with `io.ReadCloser` for [streaming](https://www.libvips.org/2019/11/29/True-streaming-for-libvips.html)
- **Streaming**: `VipsSource` and `VipsTarget` integration with Go `io.Reader` and `io.Writer` for [streaming](https://www.libvips.org/2019/11/29/True-streaming-for-libvips.html)

## Quick Start

Expand Down
18 changes: 16 additions & 2 deletions internal/generator/templatefunc.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ func generateFunctionCallArgs(op introspection.Operation, withOptions bool) stri
callArgs = append(callArgs, argStr)
continue
}
if arg.IsSource {
if arg.IsSource || arg.IsTarget {
callArgs = append(callArgs, arg.GoName)
} else if arg.GoType == "string" {
argStr = "c" + arg.GoName
Expand Down Expand Up @@ -565,6 +565,8 @@ func generateImageMethodBody(op introspection.Operation) string {
for _, arg := range methodArgs {
if arg.GoType == "*C.VipsImage" {
callArgs = append(callArgs, fmt.Sprintf("%s.image", arg.GoName))
} else if arg.IsTarget {
callArgs = append(callArgs, fmt.Sprintf("%s.target", arg.GoName))
} else if arg.GoType == "[]*C.VipsImage" {
callArgs = append(callArgs, fmt.Sprintf("convertImagesToVipsImages(%s)", arg.GoName))
} else {
Expand Down Expand Up @@ -1089,6 +1091,8 @@ func generateImageMethodParams(op introspection.Operation) string {
paramType = "[]*Image"
} else if arg.CType == "void*" {
paramType = "[]byte"
} else if arg.IsTarget {
paramType = "*Target"
} else {
paramType = arg.GoType
}
Expand Down Expand Up @@ -1355,9 +1359,12 @@ func generateCFunctionImplementation(op introspection.Operation) string {
if i > 0 {
result.WriteString(", ")
}
// Add type casting for VipsSourceCustom
if arg.IsSource {
// Add type casting for VipsSourceCustom
result.WriteString("(VipsSource*) " + arg.Name)
} else if arg.IsTarget {
// Add type casting for VipsTargetCustom
result.WriteString("(VipsTarget*) " + arg.Name)
} else {
result.WriteString(arg.Name)
}
Expand Down Expand Up @@ -1470,6 +1477,9 @@ func generateCFunctionImplementation(op introspection.Operation) string {
} else if arg.IsSource {
allParamsList = append(allParamsList,
fmt.Sprintf("vips_object_set(VIPS_OBJECT(operation), \"%s\", (VipsSource*)%s, NULL)", arg.Name, arg.Name))
} else if arg.IsTarget {
allParamsList = append(allParamsList,
fmt.Sprintf("vips_object_set(VIPS_OBJECT(operation), \"%s\", (VipsTarget*)%s, NULL)", arg.Name, arg.Name))
} else if (arg.Name == "buf" || arg.Name == "buffer") && isBufferLoadOperation {
// For buffer load operations, set the VipsBlob as the "buffer" property
allParamsList = append(allParamsList,
Expand Down Expand Up @@ -1526,6 +1536,10 @@ func generateCFunctionImplementation(op introspection.Operation) string {
// Handle source parameters
allParamsList = append(allParamsList,
fmt.Sprintf("vipsgen_set_source(operation, \"%s\", %s)", opt.Name, opt.Name))
} else if opt.IsTarget {
// Handle target parameters
allParamsList = append(allParamsList,
fmt.Sprintf("vipsgen_set_target(operation, \"%s\", %s)", opt.Name, opt.Name))
} else if opt.GoType == "int" {
allParamsList = append(allParamsList,
fmt.Sprintf("vipsgen_set_int(operation, \"%s\", %s)", opt.Name, opt.Name))
Expand Down
14 changes: 12 additions & 2 deletions internal/introspection/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type Argument struct {
IsOutput bool
IsOutputN bool
IsSource bool
IsTarget bool
IsImage bool
IsBuffer bool
IsArray bool
Expand Down Expand Up @@ -129,8 +130,7 @@ func (v *Introspection) DiscoverOperations() []Operation {
op.HasOneImageOutput = false
}

if strings.Contains(op.Name, "_target") ||
strings.Contains(op.Name, "_mime") ||
if strings.Contains(op.Name, "_mime") ||
strings.Contains(op.Name, "fitsload_source") {
log.Printf("Excluded operation: vips_%s \n", op.Name)
excludedCount++
Expand Down Expand Up @@ -202,6 +202,7 @@ func (v *Introspection) DiscoverOperationArguments(opName string) ([]Argument, e
isBuffer := int(arg.is_buffer) != 0
isArray := int(arg.is_array) != 0
isSource := cTypeCheck(arg.type_val, "VipsSource")
isTarget := cTypeCheck(arg.type_val, "VipsTarget")

// Create the Go argument structure
goArg := Argument{
Expand All @@ -215,6 +216,7 @@ func (v *Introspection) DiscoverOperationArguments(opName string) ([]Argument, e
IsBuffer: isBuffer,
IsArray: isArray,
IsSource: isSource,
IsTarget: isTarget,
Flags: int(arg.flags),
}

Expand Down Expand Up @@ -492,6 +494,14 @@ func (v *Introspection) mapGTypeToTypes(gtype C.GType, typeName string, isOutput
}
return "VipsSourceCustom", "*C.VipsSourceCustom", "VipsSourceCustom*"
}
// Special case for VipsTarget - map to VipsTargetCustom for proper compatibility
if cTypeCheck(gtype, "VipsTarget") {
// For VipsTarget, we want to use VipsTargetCustom in the bindings
if isOutput {
return "VipsTargetCustom", "*C.VipsTargetCustom", "VipsTargetCustom**"
}
return "VipsTargetCustom", "*C.VipsTargetCustom", "VipsTargetCustom*"
}
// Special case for VipsImage which has a different pointer pattern
if cTypeCheck(gtype, "VipsImage") {
if isOutput {
Expand Down
21 changes: 21 additions & 0 deletions internal/templates/callback.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,24 @@ func goSourceSeek(
}
return -1
}

//export goTargetWrite
func goTargetWrite(
ptr unsafe.Pointer, buffer unsafe.Pointer, size C.longlong,
) C.longlong {
target, ok := pointer.Restore(ptr).(*Target)
if !ok {
return -1
}
sh := &reflect.SliceHeader{
Data: uintptr(buffer),
Len: int(size),
Cap: int(size),
}
buf := *(*[]byte)(unsafe.Pointer(sh))
n, err := target.writer.Write(buf)
if err != nil {
return -1
}
return C.longlong(n)
}
16 changes: 16 additions & 0 deletions internal/templates/connection.c.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ static gint64 go_seek(VipsSourceCustom *source_custom, gint64 offset, int whence
return goSourceSeek(ptr, offset, whence);
}

static gint64 go_write(VipsTargetCustom *target_custom, void *buffer, gint64 length, void* ptr)
{
return goTargetWrite(ptr, buffer, length);
}

VipsSourceCustom * create_go_custom_source(void* ptr)
{
VipsSourceCustom * source_custom = vips_source_custom_new();
Expand All @@ -27,6 +32,17 @@ VipsSourceCustom * create_go_custom_source_with_seek(void* ptr)
return source_custom;
}

VipsTargetCustom * create_go_custom_target(void* ptr)
{
VipsTargetCustom * target_custom = vips_target_custom_new();
g_signal_connect(target_custom, "write", G_CALLBACK(go_write), ptr);
return target_custom;
}

void clear_source(VipsSourceCustom **source_custom) {
if (G_IS_OBJECT(*source_custom)) g_clear_object(source_custom);
}

void clear_target(VipsTargetCustom **target_custom) {
if (G_IS_OBJECT(*target_custom)) g_clear_object(target_custom);
}
38 changes: 38 additions & 0 deletions internal/templates/connection.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,41 @@ func (s *Source) Close() {
s.lock.Unlock()
}
}

// Target contains a libvips VipsTargetCustom and manages its lifecycle.
type Target struct {
writer io.WriteCloser
target *C.VipsTargetCustom
ptr unsafe.Pointer
lock sync.Mutex
}

// NewTarget creates Target from writer
func NewTarget(writer io.WriteCloser) *Target {
Startup(nil)
t := &Target{writer: writer}
t.ptr = pointer.Save(t)
t.target = C.create_go_custom_target(t.ptr)
return t
}

// Close target
func (t *Target) Close() {
if t == nil {
return
}
t.lock.Lock()
if t.ptr != nil {
C.clear_target(&t.target)
pointer.Unref(t.ptr)
t.ptr = nil
t.lock.Unlock()
if t.writer != nil {
_ = t.writer.Close()
t.writer = nil
}
log("vipsgen", LogLevelDebug, fmt.Sprintf("closing target %p", t))
} else {
t.lock.Unlock()
}
}
6 changes: 4 additions & 2 deletions internal/templates/connection.h.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
#include <vips/vips.h>

extern long long goSourceRead(void*, void *, long long);

extern long long goSourceSeek(void*, long long, int);
extern long long goTargetWrite(void*, void *, long long);

static gint64 go_read(VipsSourceCustom *source_custom, void *buffer, gint64 length, void* ptr);

static gint64 go_seek(VipsSourceCustom *source_custom, gint64 offset, int whence, void* ptr);
static gint64 go_write(VipsTargetCustom *target_custom, void *buffer, gint64 length, void* ptr);

VipsSourceCustom * create_go_custom_source(void* ptr);
VipsSourceCustom * create_go_custom_source_with_seek(void* ptr);
VipsTargetCustom * create_go_custom_target(void* ptr);

void clear_source(VipsSourceCustom **source_custom);
void clear_target(VipsTargetCustom **target_custom);
5 changes: 5 additions & 0 deletions internal/templates/vips.c.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ int vipsgen_set_source(VipsOperation *operation, const char *name, VipsSource *v
return 0;
}

int vipsgen_set_target(VipsOperation *operation, const char *name, VipsTarget *value) {
if (value != NULL) { return vips_object_set(VIPS_OBJECT(operation), name, value, NULL); }
return 0;
}

// Generated operations
{{range .Operations}}
{{generateCFunctionImplementation .}}
Expand Down
1 change: 1 addition & 0 deletions internal/templates/vips.h.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
int vipsgen_operation_execute(VipsOperation *operation, ...);
int vipsgen_operation_save_buffer(VipsOperation *operation, void** buf, size_t* len);
int vipsgen_set_source(VipsOperation *operation, const char *name, VipsSource *value);
int vipsgen_set_target(VipsOperation *operation, const char *name, VipsTarget *value);
int vipsgen_set_int(VipsOperation *operation, const char *name, int value);
int vipsgen_set_bool(VipsOperation *operation, const char *name, gboolean value);
int vipsgen_set_double(VipsOperation *operation, const char *name, double value);
Expand Down
21 changes: 21 additions & 0 deletions vips/callback.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions vips/connection.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions vips/connection.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions vips/connection.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading