From a9d01640bdbd6d738281a48a49033c2c98091eab Mon Sep 17 00:00:00 2001 From: Benjamin Schaaf Date: Fri, 19 Feb 2021 21:57:44 +1100 Subject: [PATCH] Add bar code scanning using zbar Fixes #41 --- main.c | 176 ++++++++++++++++++++++++++++++++++++++++++--- main.h | 3 + meson.build | 17 ++++- process_pipeline.c | 9 ++- zbar_pipeline.c | 169 +++++++++++++++++++++++++++++++++++++++++++ zbar_pipeline.h | 22 ++++++ 6 files changed, 385 insertions(+), 11 deletions(-) create mode 100644 zbar_pipeline.c create mode 100644 zbar_pipeline.h diff --git a/main.c b/main.c index 4340454..332d431 100644 --- a/main.c +++ b/main.c @@ -17,6 +17,7 @@ #include #include #include +#include #include "camera_config.h" #include "quickpreview.h" #include "io_pipeline.h" @@ -44,6 +45,8 @@ static cairo_surface_t *surface = NULL; static cairo_surface_t *status_surface = NULL; static char last_path[260] = ""; +static MPZBarScanResult *zbar_result = NULL; + static int burst_length = 3; static enum user_control current_control; @@ -130,6 +133,28 @@ mp_main_update_state(const struct mp_main_state *state) (GSourceFunc)update_state, state_copy, free); } +static bool set_zbar_result(MPZBarScanResult *result) +{ + if (zbar_result) { + for (uint8_t i = 0; i < zbar_result->size; ++i) { + free(zbar_result->codes[i].data); + } + + free(zbar_result); + } + + zbar_result = result; + gtk_widget_queue_draw(preview); + + return false; +} + +void mp_main_set_zbar_result(MPZBarScanResult *result) +{ + g_main_context_invoke_full(g_main_context_default(), G_PRIORITY_DEFAULT_IDLE, + (GSourceFunc)set_zbar_result, result, NULL); +} + static bool set_preview(cairo_surface_t *image) { @@ -148,20 +173,26 @@ mp_main_set_preview(cairo_surface_t *image) (GSourceFunc)set_preview, image, NULL); } +static void transform_centered(cairo_t *cr, uint32_t dst_width, uint32_t dst_height, + int src_width, int src_height) +{ + cairo_translate(cr, dst_width / 2, dst_height / 2); + + double scale = MIN(dst_width / (double)src_width, dst_height / (double)src_height); + cairo_scale(cr, scale, scale); + + cairo_translate(cr, -src_width / 2, -src_height / 2); +} + void draw_surface_scaled_centered(cairo_t *cr, uint32_t dst_width, uint32_t dst_height, cairo_surface_t *surface) { cairo_save(cr); - cairo_translate(cr, dst_width / 2, dst_height / 2); - int width = cairo_image_surface_get_width(surface); int height = cairo_image_surface_get_height(surface); - double scale = MIN(dst_width / (double)width, dst_height / (double)height); - cairo_scale(cr, scale, scale); - - cairo_translate(cr, -width / 2, -height / 2); + transform_centered(cr, dst_width, dst_height, width, height); cairo_set_source_surface(cr, surface, 0, 0); cairo_paint(cr); @@ -296,10 +327,39 @@ preview_draw(GtkWidget *widget, cairo_t *cr, gpointer data) // Clear preview area with black cairo_paint(cr); - // Draw camera preview if (surface) { - draw_surface_scaled_centered(cr, preview_width, preview_height, - surface); + // Draw camera preview + cairo_save(cr); + + int width = cairo_image_surface_get_width(surface); + int height = cairo_image_surface_get_height(surface); + transform_centered(cr, preview_width, preview_height, width, height); + + cairo_set_source_surface(cr, surface, 0, 0); + cairo_paint(cr); + + // Draw zbar image + if (zbar_result) { + for (uint8_t i = 0; i < zbar_result->size; ++i) { + MPZBarCode *code = &zbar_result->codes[i]; + + cairo_set_source_rgba(cr, 1, 1, 1, 0.5); + cairo_new_path(cr); + cairo_move_to(cr, code->bounds_x[0], code->bounds_y[0]); + for (uint8_t i = 0; i < 4; ++i) { + cairo_line_to(cr, code->bounds_x[i], code->bounds_y[i]); + } + cairo_close_path(cr); + cairo_stroke(cr); + + cairo_save(cr); + cairo_translate(cr, code->bounds_x[0], code->bounds_y[0]); + cairo_show_text(cr, code->data); + cairo_restore(cr); + } + } + + cairo_restore(cr); } // Draw control overlay @@ -360,6 +420,85 @@ on_shutter_clicked(GtkWidget *widget, gpointer user_data) mp_io_pipeline_capture(); } +static bool +check_point_inside_bounds(int x, int y, int *bounds_x, int *bounds_y) +{ + bool right = false, left = false, top = false, bottom = false; + + for (int i = 0; i < 4; ++i) { + if (x <= bounds_x[i]) + left = true; + if (x >= bounds_x[i]) + right = true; + if (y <= bounds_y[i]) + top = true; + if (y >= bounds_y[i]) + bottom = true; + } + + return right && left && top && bottom; +} + +static void +on_zbar_code_tapped(GtkWidget *widget, const MPZBarCode *code) +{ + GtkWidget *dialog; + GtkDialogFlags flags = GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT; + bool data_is_url = strncmp(code->data, "http://", 7) == 0 + || strncmp(code->data, "https://", 8) == 0; + + if (data_is_url) { + dialog = gtk_message_dialog_new( + GTK_WINDOW(gtk_widget_get_toplevel(widget)), + flags, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_NONE, + "Found a URL '%s' encoded in a %s code.", + code->data, + code->type); + gtk_dialog_add_buttons( + GTK_DIALOG(dialog), + "_Open URL", + GTK_RESPONSE_YES, + NULL); + } else { + dialog = gtk_message_dialog_new( + GTK_WINDOW(gtk_widget_get_toplevel(widget)), + flags, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_NONE, + "Found '%s' encoded in a %s code.", + code->data, + code->type); + } + gtk_dialog_add_buttons( + GTK_DIALOG(dialog), + "_Copy", + GTK_RESPONSE_ACCEPT, + "_Cancel", + GTK_RESPONSE_CANCEL, + NULL); + + int result = gtk_dialog_run(GTK_DIALOG(dialog)); + + GError *error = NULL; + switch (result) { + case GTK_RESPONSE_YES: + if (!g_app_info_launch_default_for_uri(code->data, + NULL, &error)) { + g_printerr("Could not launch browser: %s\n", + error->message); + } + case GTK_RESPONSE_ACCEPT: + gtk_clipboard_set_text( + gtk_clipboard_get(GDK_SELECTION_PRIMARY), + code->data, -1); + case GTK_RESPONSE_CANCEL: + break; + } + gtk_widget_destroy(dialog); +} + void on_preview_tap(GtkWidget *widget, GdkEventButton *event, gpointer user_data) { @@ -399,6 +538,25 @@ on_preview_tap(GtkWidget *widget, GdkEventButton *event, gpointer user_data) return; } + // Tapped zbar result + if (zbar_result) { + // Transform the event coordinates to the image + int width = cairo_image_surface_get_width(surface); + int height = cairo_image_surface_get_height(surface); + double scale = MIN(preview_width / (double)width, preview_height / (double)height); + int x = (event->x - preview_width / 2) / scale + width / 2; + int y = (event->y - preview_height / 2) / scale + height / 2; + + for (uint8_t i = 0; i < zbar_result->size; ++i) { + MPZBarCode *code = &zbar_result->codes[i]; + + if (check_point_inside_bounds(x, y, code->bounds_x, code->bounds_y)) { + on_zbar_code_tapped(widget, code); + return; + } + } + } + // Tapped preview image itself, try focussing if (has_auto_focus_start) { mp_io_pipeline_focus(); diff --git a/main.h b/main.h index 011859b..2995993 100644 --- a/main.h +++ b/main.h @@ -1,6 +1,7 @@ #pragma once #include "camera_config.h" +#include "zbar_pipeline.h" #include "gtk/gtk.h" #define MP_MAIN_THUMB_SIZE 24 @@ -25,6 +26,8 @@ void mp_main_update_state(const struct mp_main_state *state); void mp_main_set_preview(cairo_surface_t *image); void mp_main_capture_completed(cairo_surface_t *thumb, const char *fname); +void mp_main_set_zbar_result(MPZBarScanResult *result); + int remap(int value, int input_min, int input_max, int output_min, int output_max); void draw_surface_scaled_centered(cairo_t *cr, uint32_t dst_width, uint32_t dst_height, diff --git a/meson.build b/meson.build index 0c324bf..56df6fe 100644 --- a/meson.build +++ b/meson.build @@ -2,6 +2,7 @@ project('megapixels', 'c') gnome = import('gnome') gtkdep = dependency('gtk+-3.0') tiff = dependency('libtiff-4') +zbar = dependency('zbar') threads = dependency('threads') cc = meson.get_compiler('c') @@ -26,7 +27,21 @@ if get_option('tiffcfapattern') add_global_arguments('-DLIBTIFF_CFA_PATTERN', language: 'c') endif -executable('megapixels', 'main.c', 'ini.c', 'quickpreview.c', 'camera.c', 'device.c', 'pipeline.c', 'camera_config.c', 'io_pipeline.c', 'process_pipeline.c', 'matrix.c', resources, dependencies : [gtkdep, libm, tiff, threads], install : true) +executable('megapixels', + 'main.c', + 'ini.c', + 'quickpreview.c', + 'camera.c', + 'device.c', + 'pipeline.c', + 'camera_config.c', + 'io_pipeline.c', + 'process_pipeline.c', + 'zbar_pipeline.c', + 'matrix.c', + resources, + dependencies : [gtkdep, libm, tiff, zbar, threads], + install : true) install_data(['data/org.postmarketos.Megapixels.desktop'], install_dir : get_option('datadir') / 'applications') diff --git a/process_pipeline.c b/process_pipeline.c index fa82983..4ba2f99 100644 --- a/process_pipeline.c +++ b/process_pipeline.c @@ -1,6 +1,7 @@ #include "process_pipeline.h" #include "pipeline.h" +#include "zbar_pipeline.h" #include "main.h" #include "config.h" #include "quickpreview.h" @@ -120,12 +121,17 @@ mp_process_pipeline_start() pipeline = mp_pipeline_new(); mp_pipeline_invoke(pipeline, setup, NULL, 0); + + + mp_zbar_pipeline_start(); } void mp_process_pipeline_stop() { mp_pipeline_free(pipeline); + + mp_zbar_pipeline_stop(); } static cairo_surface_t * @@ -160,7 +166,8 @@ process_image_for_preview(const MPImage *image) cairo_destroy(cr); } - // Pass processed preview to main + // Pass processed preview to main and zbar + mp_zbar_pipeline_process_image(cairo_surface_reference(surface)); mp_main_set_preview(surface); return thumb; diff --git a/zbar_pipeline.c b/zbar_pipeline.c new file mode 100644 index 0000000..0b6aaa8 --- /dev/null +++ b/zbar_pipeline.c @@ -0,0 +1,169 @@ +#include "zbar_pipeline.h" + +#include "pipeline.h" +#include "main.h" +#include "io_pipeline.h" +#include +#include + +static MPPipeline *pipeline; + +static volatile int frames_processed = 0; +static volatile int frames_received = 0; + +static zbar_image_scanner_t *scanner; + +static void setup(MPPipeline *pipeline, const void *data) +{ + scanner = zbar_image_scanner_create(); + zbar_image_scanner_set_config(scanner, 0, ZBAR_CFG_ENABLE, 1); +} + +void +mp_zbar_pipeline_start() +{ + pipeline = mp_pipeline_new(); + + mp_pipeline_invoke(pipeline, setup, NULL, 0); +} + +void +mp_zbar_pipeline_stop() +{ + mp_pipeline_free(pipeline); +} + +static bool is_3d_code(zbar_symbol_type_t type) +{ + switch (type) { + case ZBAR_EAN2: + case ZBAR_EAN5: + case ZBAR_EAN8: + case ZBAR_UPCE: + case ZBAR_ISBN10: + case ZBAR_UPCA: + case ZBAR_EAN13: + case ZBAR_ISBN13: + case ZBAR_I25: + case ZBAR_DATABAR: + case ZBAR_DATABAR_EXP: + case ZBAR_CODABAR: + case ZBAR_CODE39: + case ZBAR_CODE93: + case ZBAR_CODE128: + return false; + case ZBAR_COMPOSITE: + case ZBAR_PDF417: + case ZBAR_QRCODE: + case ZBAR_SQCODE: + return true; + default: + return false; + } +} + +static MPZBarCode +process_symbol(const zbar_symbol_t *symbol) +{ + MPZBarCode code; + + unsigned loc_size = zbar_symbol_get_loc_size(symbol); + assert(loc_size > 0); + + zbar_symbol_type_t type = zbar_symbol_get_type(symbol); + + if (is_3d_code(type) && loc_size == 4) { + for (unsigned i = 0; i < loc_size; ++i) { + code.bounds_x[i] = zbar_symbol_get_loc_x(symbol, i); + code.bounds_y[i] = zbar_symbol_get_loc_y(symbol, i); + } + } else { + int min_x = zbar_symbol_get_loc_x(symbol, 0); + int min_y = zbar_symbol_get_loc_y(symbol, 0); + int max_x = min_x, max_y = min_y; + for (unsigned i = 1; i < loc_size; ++i) { + int x = zbar_symbol_get_loc_x(symbol, i); + int y = zbar_symbol_get_loc_y(symbol, i); + min_x = MIN(min_x, x); + min_y = MIN(min_y, y); + max_x = MAX(max_x, x); + max_y = MAX(max_y, y); + } + + code.bounds_x[0] = min_x; + code.bounds_y[0] = min_y; + code.bounds_x[1] = max_x; + code.bounds_y[1] = min_y; + code.bounds_x[2] = max_x; + code.bounds_y[2] = max_y; + code.bounds_x[3] = min_x; + code.bounds_y[3] = max_y; + } + + const char *data = zbar_symbol_get_data(symbol); + unsigned int data_size = zbar_symbol_get_data_length(symbol); + code.data = strndup(data, data_size); + code.type = zbar_get_symbol_name(type); + + return code; +} + +static void +process_surface(MPPipeline *pipeline, cairo_surface_t **_surface) +{ + cairo_surface_t *surface = *_surface; + + int width = cairo_image_surface_get_width(surface); + int height = cairo_image_surface_get_height(surface); + const uint32_t *surface_data = (const uint32_t *)cairo_image_surface_get_data(surface); + + // Create a grayscale image for scanning from the current preview + uint8_t *data = malloc(width * height * sizeof(uint8_t)); + for (size_t i = 0; i < width * height; ++i) { + data[i] = (surface_data[i] >> 16) & 0xff; + } + + // Create image for zbar + zbar_image_t *zbar_image = zbar_image_create(); + zbar_image_set_format(zbar_image, zbar_fourcc('Y', '8', '0', '0')); + zbar_image_set_size(zbar_image, width, height); + zbar_image_set_data(zbar_image, data, width * height * sizeof(uint8_t), zbar_image_free_data); + + int res = zbar_scan_image(scanner, zbar_image); + assert(res >= 0); + + if (res > 0) { + MPZBarScanResult *result = malloc(sizeof(MPZBarScanResult)); + result->size = res; + + const zbar_symbol_t *symbol = zbar_image_first_symbol(zbar_image); + for (int i = 0; i < MIN(res, 8); ++i) { + assert(symbol != NULL); + result->codes[i] = process_symbol(symbol); + symbol = zbar_symbol_next(symbol); + } + + mp_main_set_zbar_result(result); + } else { + mp_main_set_zbar_result(NULL); + } + + zbar_image_destroy(zbar_image); + cairo_surface_destroy(surface); + + ++frames_processed; +} + +void +mp_zbar_pipeline_process_image(cairo_surface_t *surface) +{ + // If we haven't processed the previous frame yet, drop this one + if (frames_received != frames_processed) { + return; + } + + ++frames_received; + + mp_pipeline_invoke(pipeline, (MPPipelineCallback)process_surface, &surface, + sizeof(cairo_surface_t *)); +} diff --git a/zbar_pipeline.h b/zbar_pipeline.h new file mode 100644 index 0000000..150861a --- /dev/null +++ b/zbar_pipeline.h @@ -0,0 +1,22 @@ +#pragma once + +#include "camera_config.h" + +typedef struct _cairo_surface cairo_surface_t; + +typedef struct { + int bounds_x[4]; + int bounds_y[4]; + char *data; + const char *type; +} MPZBarCode; + +typedef struct { + MPZBarCode codes[8]; + uint8_t size; +} MPZBarScanResult; + +void mp_zbar_pipeline_start(); +void mp_zbar_pipeline_stop(); + +void mp_zbar_pipeline_process_image(cairo_surface_t *surface);