Changes
7 changed files (+661/-6)
-
-
@@ -23,8 +23,10 @@ const Arc = @import("./Arc.zig");const browse = @import("./browse.zig"); const discovery = @import("./discovery.zig"); const extension = @import("./extension.zig").extension; const image = @import("./image.zig"); const freelog = @import("./log.zig").freelog; const BrowseService = @import("./services/browse.zig").BrowseService; const ImageService = @import("./services/ImageService.zig"); const ping = @import("./services/ping.zig"); const registry = @import("./services/registry.zig"); const TransportService = @import("./services/transport.zig").TransportService;
-
@@ -333,30 +335,35 @@ Event.exportEventCastFunction(transport.ZoneListEvent, .zone_list);} }; fn JsonResponseListener(comptime T: type) type { fn Listener(comptime T: type) type { return struct { wrote: std.Thread.ResetEvent = .{}, data: ?moo.JsonBody(T) = null, data: ?T = null, pub fn listen(self: *@This()) moo.JsonBody(T) { pub fn listen(self: *@This()) T { self.wrote.wait(); return self.data orelse @panic("Set JsonResponseListener.wrote before writing data"); return self.data orelse @panic("Set Listener.wrote before writing data"); } pub fn write(self: *@This(), data: moo.JsonBody(T)) void { pub fn write(self: *@This(), data: T) void { self.data = data; self.wrote.set(); } }; } fn JsonResponseListener(comptime T: type) type { return Listener(moo.JsonBody(T)); } pub const Connection = extern struct { const cname = "plac_connection"; const allocator = std.heap.c_allocator; const BrowseListener = JsonResponseListener(BrowseService.Browse.Response); const LoadListener = JsonResponseListener(BrowseService.Load.Response); const ImageListener = Listener(*image.GetResult); internal: *Internal,
-
@@ -371,6 +378,7 @@ arc: Arc = .{},saved_token: ?[]const u8 = null, browse_listeners: std.AutoHashMap(i64, BrowseListener), load_listeners: std.AutoHashMap(i64, LoadListener), image_listeners: std.AutoHashMap(i64, ImageListener), fn init(server: *discovery.Server, token: ?[]const u8) !Internal { var addr = std.ArrayList(u8).init(allocator);
-
@@ -400,12 +408,14 @@ .host = host,.saved_token = saved_token, .browse_listeners = std.AutoHashMap(i64, BrowseListener).init(allocator), .load_listeners = std.AutoHashMap(i64, LoadListener).init(allocator), .image_listeners = std.AutoHashMap(i64, ImageListener).init(allocator), }; } fn deinit(self: *Internal) void { self.browse_listeners.deinit(); self.load_listeners.deinit(); self.image_listeners.deinit(); if (self.ws) |*ws| { ws.deinit();
-
@@ -636,6 +646,33 @@ entry.value_ptr.write(res);continue; } if (self.internal.image_listeners.getEntry(header.request_id)) |entry| { std.log.debug("Received /get_image response (ID={d})", .{header.request_id}); const res = ImageService.Get.Response.decode( &meta, header_ctx, msg.data, ) catch |err| { std.log.debug("Received unexpected get_image response: {s}", .{@errorName(err)}); const err_obj = image.GetResult.makeRetainedError(.unexpected_response) catch |obj_err| { std.log.err("Unable to compose image.GetResult: {s}", .{@errorName(obj_err)}); continue; }; entry.value_ptr.write(err_obj); continue; }; const obj = image.GetResult.makeRetained(&res) catch |err| { std.log.err("Unable to compose image.GetResult: {s}", .{@errorName(err)}); continue; }; entry.value_ptr.write(obj); continue; } std.log.warn("Unhandle message on {s}", .{meta.service}); std.log.debug("{s}", .{msg.data}); }
-
@@ -1025,6 +1062,71 @@ },} } pub fn getImage( ptr: ?*Connection, image_key: [*:0]const u8, opts_ptr: ?*image.GetOptions, ) callconv(.C) ?*image.GetResult { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); const opts = image.GetOptions.retain(opts_ptr); defer opts.release(); var ws = self.internal.ws orelse { std.log.err( "{s}_{s} called, but WebSocket connection is not ready", .{ cname, @src().fn_name }, ); return image.GetResult.makeRetainedError(.socket_closed) catch null; }; const req_id = self.internal.request_id; self.internal.request_id += 1; std.log.debug("Sending get_image request...", .{}); var req = ImageService.Get.Request{ .image_key = std.mem.span(image_key), .format = if (opts.internal.content_type) |t| switch (t) { .jpeg => .jpeg, .png => .png, } else null, }; if (opts.internal.size) |size| { req.scale = switch (size.scaling_method) { .fit => .fit, .fill => .fill, .stretch => .stretch, }; req.width = size.width; req.height = size.height; } const req_msg = req.encode(allocator, req_id) catch |err| { std.log.err("Unable to compose get_image request: {s}", .{@errorName(err)}); return image.GetResult.makeRetainedError(.unknown_error) catch null; }; defer allocator.free(req_msg); var entry = self.internal.image_listeners.getOrPut(req_id) catch |err| { std.log.err("Unable to set listener for get_image response: {s}", .{@errorName(err)}); return image.GetResult.makeRetainedError(.unknown_error) catch null; }; entry.value_ptr.* = .{}; defer _ = self.internal.image_listeners.remove(req_id); ws.writeBin(req_msg) catch |err| { std.log.err("Unable to write get_image request: {s}", .{@errorName(err)}); return image.GetResult.makeRetainedError(.failed_to_send) catch null; }; std.log.debug("Sent get_image request (ID={d})", .{req_id}); // Pointer is already retained. return entry.value_ptr.listen(); } pub fn export_capi() void { @export(&make, .{ .name = std.fmt.comptimePrint("{s}_make", .{cname}) }); @export(&retain, .{ .name = std.fmt.comptimePrint("{s}_retain", .{cname}) });
-
@@ -1033,6 +1135,7 @@ @export(&getEvent, .{ .name = std.fmt.comptimePrint("{s}_get_event", .{cname}) });@export(&subscribeZones, .{ .name = std.fmt.comptimePrint("{s}_subscribe_zones", .{cname}) }); @export(&control, .{ .name = std.fmt.comptimePrint("{s}_control", .{cname}) }); @export(&requestBrowse, .{ .name = std.fmt.comptimePrint("{s}_browse", .{cname}) }); @export(&getImage, .{ .name = std.fmt.comptimePrint("{s}_get_image", .{cname}) }); } };
-
-
-
@@ -16,6 +16,7 @@ // SPDX-License-Identifier: Apache-2.0const Extension = @import("./services/registry.zig").Extension; const BrowseService = @import("./services/browse.zig").BrowseService; const ImageService = @import("./services/ImageService.zig"); const PingService = @import("./services/ping.zig").PingService; const TransportService = @import("./services/transport.zig").TransportService;
-
@@ -25,7 +26,7 @@ .display_name = "Plac",.version = "0.0.0-dev", .publisher = "Shota FUJI", .email = "pockawoooh@gmail.com", .required_services = &.{ TransportService.id, BrowseService.id }, .required_services = &.{ TransportService.id, BrowseService.id, ImageService.id }, .optional_services = &.{}, .provided_services = &.{PingService.id}, };
-
-
core/src/image.zig (new)
-
@@ -0,0 +1,291 @@// Copyright 2025 Shota FUJI // // 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 // // http://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. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const Arc = @import("./Arc.zig"); const freelog = @import("./log.zig").freelog; const ImageService = @import("./services/ImageService.zig"); pub const ScalingMethod = enum(c_int) { fit = 0, fill = 1, stretch = 2, }; pub const ContentType = enum(c_int) { jpeg = 0, png = 1, }; pub const GetResultCode = enum(c_int) { ok = 0, unknown_error = 1, out_of_memory = 2, unexpected_response = 3, socket_closed = 4, failed_to_send = 5, }; pub const GetOptions = extern struct { const cname = "plac_image_get_options"; const allocator = std.heap.c_allocator; internal: *Internal, const Internal = struct { arc: Arc = .{}, size: ?Size = null, content_type: ?ContentType = null, const Size = struct { scaling_method: ScalingMethod, width: usize, height: usize, }; }; pub fn make() callconv(.C) ?*@This() { const internal = allocator.create(Internal) catch { return null; }; internal.* = .{}; const self = allocator.create(@This()) catch { allocator.destroy(internal); return null; }; self.* = .{ .internal = internal }; return self; } pub fn retain(ptr: ?*@This()) callconv(.C) *@This() { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); self.internal.arc.ref(); return self; } pub fn release(ptr: ?*@This()) callconv(.C) void { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); if (self.internal.arc.unref()) { freelog(self); allocator.destroy(self.internal); allocator.destroy(self); } } pub fn setSize( ptr: ?*@This(), scaling: ScalingMethod, width: usize, height: usize, ) callconv(.C) void { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); _ = self.retain(); defer self.release(); self.internal.size = .{ .scaling_method = scaling, .width = width, .height = height, }; } pub fn setContentType(ptr: ?*@This(), content_type: ContentType) callconv(.C) void { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); _ = self.retain(); defer self.release(); self.internal.content_type = content_type; } pub fn export_capi() void { @export(&make, .{ .name = std.fmt.comptimePrint("{s}_make", .{cname}) }); @export(&retain, .{ .name = std.fmt.comptimePrint("{s}_retain", .{cname}) }); @export(&release, .{ .name = std.fmt.comptimePrint("{s}_release", .{cname}) }); @export(&setSize, .{ .name = std.fmt.comptimePrint("{s}_set_size", .{cname}) }); @export(&setContentType, .{ .name = std.fmt.comptimePrint("{s}_set_content_type", .{cname}) }); } }; pub const Image = extern struct { const cname = "plac_image_image"; const allocator = std.heap.c_allocator; internal: *Internal, content_type: ContentType, data_ptr: [*]const u8, data_len: usize, const Internal = struct { arc: Arc = .{}, }; pub fn make(src: *const ImageService.Get.Response) std.mem.Allocator.Error!*@This() { const internal = try allocator.create(Internal); errdefer allocator.destroy(internal); internal.* = .{}; const data = try allocator.dupe(u8, src.data); errdefer allocator.free(data); const self = try allocator.create(@This()); errdefer allocator.destroy(self); self.* = .{ .internal = internal, .content_type = switch (src.content_type) { .jpeg => ContentType.jpeg, .png => ContentType.png, }, .data_ptr = data.ptr, .data_len = data.len, }; return self; } pub fn retain(ptr: ?*@This()) callconv(.C) *@This() { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); self.internal.arc.ref(); return self; } pub fn release(ptr: ?*@This()) callconv(.C) void { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); if (self.internal.arc.unref()) { freelog(self); allocator.free(self.data_ptr[0..self.data_len]); allocator.destroy(self.internal); allocator.destroy(self); } } pub fn export_capi() void { @export(&retain, .{ .name = std.fmt.comptimePrint("{s}_retain", .{cname}) }); @export(&release, .{ .name = std.fmt.comptimePrint("{s}_release", .{cname}) }); } }; pub const GetResult = extern struct { const cname = "plac_image_get_result"; const allocator = std.heap.c_allocator; internal: *Internal, code: GetResultCode = .ok, image: ?*Image = null, const Internal = struct { arc: Arc = .{}, }; fn make(src: *const ImageService.Get.Response) std.mem.Allocator.Error!*@This() { const internal = try allocator.create(Internal); errdefer allocator.destroy(internal); internal.* = .{}; const self = try allocator.create(@This()); errdefer allocator.destroy(self); const image = try Image.make(src); _ = image.retain(); self.* = .{ .internal = internal, .image = image, }; return self; } pub inline fn makeRetained(src: *const ImageService.Get.Response) std.mem.Allocator.Error!*@This() { const result = try make(src); return result.retain(); } fn makeError(code: GetResultCode) std.mem.Allocator.Error!*@This() { const internal = try allocator.create(Internal); errdefer allocator.destroy(internal); internal.* = .{}; const self = try allocator.create(@This()); errdefer allocator.destroy(self); self.* = .{ .internal = internal, .code = code, }; return self; } pub inline fn makeRetainedError(code: GetResultCode) std.mem.Allocator.Error!*@This() { const result = try makeError(code); return result.retain(); } pub fn retain(ptr: ?*@This()) callconv(.C) *@This() { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); self.internal.arc.ref(); return self; } pub fn release(ptr: ?*@This()) callconv(.C) void { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); if (self.internal.arc.unref()) { freelog(self); if (self.image) |image| { image.release(); } allocator.destroy(self.internal); allocator.destroy(self); } } pub fn export_capi() void { @export(&retain, .{ .name = std.fmt.comptimePrint("{s}_retain", .{cname}) }); @export(&release, .{ .name = std.fmt.comptimePrint("{s}_release", .{cname}) }); } }; pub fn export_capi() void { Image.export_capi(); GetOptions.export_capi(); GetResult.export_capi(); }
-
-
-
@@ -17,11 +17,13 @@pub const browse = @import("./browse.zig"); pub const connection = @import("./connection.zig"); pub const discovery = @import("./discovery.zig"); pub const image = @import("./image.zig"); pub const transport = @import("./transport.zig"); pub fn export_capi() void { browse.export_capi(); connection.export_capi(); discovery.export_capi(); image.export_capi(); transport.export_capi(); }
-
-
-
@@ -249,6 +249,56 @@ plac_browse_list_action *plac_browse_result_get_list_action(plac_browse_result*);plac_browse_error_message_action *plac_browse_result_get_error_message_action(plac_browse_result*); plac_browse_message_action *plac_browse_result_get_message_action(plac_browse_result*); // image.ScalingMethod typedef enum { PLAC_IMAGE_SCALING_METHOD_FIT = 0, PLAC_IMAGE_SCALING_METHOD_FILL = 1, PLAC_IMAGE_SCALING_METHOD_STRETCH = 2, } plac_image_scaling_method; // image.ContentType typedef enum { PLAC_IMAGE_CONTENT_TYPE_JPEG = 0, PLAC_IMAGE_CONTENT_TYPE_PNG = 1, } plac_image_content_type; // image.GetResultCode typedef enum { PLAC_IMAGE_GET_RESULT_OK = 0, PLAC_IMAGE_GET_RESULT_UNKNOWN_ERROR = 1, PLAC_IMAGE_GET_RESULT_OUT_OF_MEMORY = 2, PLAC_IMAGE_GET_RESULT_UNEXPECTED_RESPONSE = 3, PLAC_IMAGE_GET_RESULT_SOCKET_CLOSED = 4, PLAC_IMAGE_GET_RESULT_FAILED_TO_SEND = 5, } plac_image_get_result_code; // image.GetOptions typedef struct { void *__pri; } plac_image_get_options; plac_image_get_options *plac_image_get_options_make(); plac_image_get_options *plac_image_get_options_retain(plac_image_get_options*); void *plac_image_get_options_release(plac_image_get_options*); void *plac_image_get_options_set_size(plac_image_get_options*, plac_image_scaling_method, size_t width, size_t height); void *plac_image_get_options_set_content_type(plac_image_get_options*, plac_image_content_type); // image.Image typedef struct { void *__pri; plac_image_content_type content_type; const uint8_t *data_ptr; size_t data_len; } plac_image_image; plac_image_image *plac_image_image_retain(plac_image_image*); void plac_image_image_release(plac_image_image*); // image.GetResult typedef struct { void *__pri; plac_image_get_result_code code; plac_image_image *image; } plac_image_get_result; plac_image_get_result *plac_image_get_result_retain(plac_image_get_result*); void plac_image_get_result_release(plac_image_get_result*); // connection.ConnectedEvent typedef struct { void *__pri;
-
@@ -302,5 +352,6 @@ plac_connection_event *plac_connection_get_event(plac_connection*);void plac_connection_subscribe_zones(plac_connection*); void plac_connection_control(plac_connection*, plac_transport_zone*, uint16_t action); plac_browse_result *plac_connection_browse(plac_connection*, plac_browse_hierarchy, plac_transport_zone*, plac_browse_item*, bool pop); plac_image_get_result *plac_connection_get_image(plac_connection*, const char *image_key, plac_image_get_options*); #endif
-
-
-
@@ -354,6 +354,82 @@ public MessageAction get_message_action();} } namespace Image { [CCode ( cname = "plac_image_scaling_method", cprefix = "PLAC_IMAGE_SCALING_METHOD_", has_type_id = false )] public enum ScalingMethod { FIT = 0, FILL = 1, STRETCH = 2, } [CCode ( cname = "plac_image_content_type", cprefix = "PLAC_IMAGE_CONTENT_TYPE_", has_type_id = false )] public enum ContentType { JPEG = 0, PNG = 1, } [CCode ( cname = "plac_image_get_options", ref_function = "plac_image_get_options_retain", unref_function = "plac_image_get_options_release" )] [Compact] public class GetOptions { public void set_size(ScalingMethod scaling, size_t width, size_t height); public void set_content_type(ContentType content_type); } [CCode ( cname = "plac_image_get_result_code", cprefix = "PLAC_IMAGE_GET_RESULT_", has_type_id = false )] public enum GetResultCode { OK = 0, UNKNOWN_ERROR = 1, OUT_OF_MEMORY = 2, UNEXPECTED_RESPONSE = 3, SOCKET_CLOSED = 4, FAILED_TO_SEND = 5, } [CCode ( cname = "plac_image_image", ref_function = "plac_image_image_retain", unref_function = "plac_image_image_release" )] [Compact] public class Image { public ContentType content_type; [CCode ( cname = "data_ptr", array_length_cname = "data_len", array_length_type = "size_t" )] public uint8[] data; } [CCode ( cname = "plac_image_get_result", ref_function = "plac_image_get_result_retain", unref_function = "plac_image_get_result_release" )] [Compact] public class GetResult { public GetResultCode code; public Image? image; } } [CCode ( cname = "plac_connection_connection_error", cprefix = "PLAC_CONNECTION_ERROR_",
-
@@ -463,5 +539,8 @@ public void control(Transport.Zone zone, uint16 action);[CCode (cname = "plac_connection_browse")] public Browse.Result? browse(Browse.Hierarchy hierarchy, Transport.Zone? zone, Browse.Item? item, bool pop); [CCode (cname = "plac_connection_get_image")] public Image.GetResult? get_image(string image_key); } }
-
-
-
@@ -0,0 +1,128 @@// Copyright 2025 Shota FUJI // // 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 // // http://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. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const moo = @import("moo"); pub const id = "com.roonlabs.image:1"; pub const ScalingMethod = enum { fit, fill, stretch, pub fn jsonStringify(self: @This(), jws: anytype) !void { try jws.write(@tagName(self)); } }; pub const ContentType = enum { jpeg, png, pub fn jsonStringify(self: @This(), jws: anytype) !void { try jws.write(switch (self) { .jpeg => "image/jpeg", .png => "image/png", }); } pub const FromStringError = error{ UnknownContentType, }; pub fn fromString(input: []const u8) FromStringError!@This() { inline for (@typeInfo(@This()).@"enum".fields) |field| { if (std.mem.eql(u8, input, std.fmt.comptimePrint("image/{s}", .{field.name}))) { return @enumFromInt(field.value); } } return FromStringError.UnknownContentType; } }; pub const Get = struct { const method = "/get_image"; pub const Request = struct { scale: ?ScalingMethod = null, width: ?usize = null, height: ?usize = null, format: ?ContentType = null, image_key: []const u8, pub fn encode( self: @This(), allocator: std.mem.Allocator, request_id: i64, ) ![]u8 { const meta = moo.Metadata{ .service = id ++ method, .verb = "REQUEST", }; const body = moo.JsonBody(Request).init(&self, .{ .emit_null_optional_fields = false, }); const header = body.getHeader(request_id); const buf = try allocator.alloc( u8, meta.getEncodeSize() + header.getEncodeSize() + body.getEncodeSize(), ); errdefer allocator.free(buf); var fbs = std.io.fixedBufferStream(buf); try moo.encode(fbs.writer(), meta, header, body); return buf; } }; pub const Response = struct { content_type: ContentType, data: []const u8, pub const DecodeError = error{ NonSuccessResponse, }; /// Returned response's `data` field is a slice of `message` bytes. pub fn decode( meta: *const moo.Metadata, header_ctx: moo.HeaderParsingContext, message: []const u8, ) !@This() { if (!std.mem.eql(u8, meta.service, "Success")) { std.log.err( "Expected \"Success\" for {s}{s}, got \"{s}\"", .{ id, method, meta.service }, ); return DecodeError.NonSuccessResponse; } const header, const body_ctx = try moo.WellKnownHeaders.parse(message, header_ctx); const content_type = try ContentType.fromString(header.content_type); const body = try moo.RawBody.parse(message, body_ctx); return .{ .content_type = content_type, .data = body.bytes }; } }; };
-