diff --git a/Makefile b/Makefile index 456f3a2..d2a0160 100644 --- a/Makefile +++ b/Makefile @@ -815,7 +815,7 @@ $(DMBEAT): app/dmbeat.f90 $(TARGET) $(FC) $(FFLAGS) $(LDFLAGS) -o $(DMBEAT) app/dmbeat.f90 $(TARGET) $(LIBCURL) $(LIBLUA54) $(LIBZ) $(LIBRT) $(LDLIBS) $(DMBOT): app/dmbot.f90 $(TARGET) - $(FC) $(FFLAGS) $(LDFLAGS) -o $(DMBOT) app/dmbot.f90 $(TARGET) $(LIBLUA54) $(LIBSQLITE3) $(LIBSTROPHE) $(LIBRT) $(LDLIBS) + $(FC) $(FFLAGS) $(LDFLAGS) -o $(DMBOT) app/dmbot.f90 $(TARGET) $(LIBLUA54) $(LIBSQLITE3) $(LIBCURL) $(LIBSTROPHE) $(LIBRT) $(LDLIBS) $(DMDB): app/dmdb.f90 $(TARGET) $(FC) $(FFLAGS) $(LDFLAGS) -o $(DMDB) app/dmdb.f90 $(TARGET) $(LIBLUA54) $(LIBSQLITE3) $(LIBPTHREAD) $(LIBRT) $(LDLIBS) diff --git a/README.md b/README.md index b6f1af6..326da67 100644 --- a/README.md +++ b/README.md @@ -218,13 +218,13 @@ containing the **DMPACK** module files is passed through argument `-I`. | `dm_zlib` | zlib | `pkg-config --libs zlib` | | `dm_zstd` | zstd | `pkg-config --libs libzstd` | -Some modules use standard input/output to communicate with the following external programs: +Some modules use standard input/output to communicate with external programs: -| Module | Program | Default Binary Name | -|-----------------|---------------------|---------------------------------------------------| -| `dm_camera` | FFmpeg | `ffmpeg` | -| `dm_gm` | GraphicsMagick | `gm` | -| `dm_plot` | Gnuplot | `gnuplot` | +| Module | Program | Default Binary Name | +|-----------------|---------------------|---------------------| +| `dm_camera` | FFmpeg | `ffmpeg` | +| `dm_gm` | GraphicsMagick | `gm` | +| `dm_plot` | Gnuplot | `gnuplot` | ## Source Code Structure diff --git a/app/dmbot.f90 b/app/dmbot.f90 index edef59d..56c42ea 100644 --- a/app/dmbot.f90 +++ b/app/dmbot.f90 @@ -22,17 +22,18 @@ program dmbot ! Bot commands. integer, parameter :: BOT_COMMAND_NONE = 0 !! Invalid command. integer, parameter :: BOT_COMMAND_BEATS = 1 !! Return time in Swatch Internet Time (.beats). - integer, parameter :: BOT_COMMAND_DATE = 2 !! Return date and time. - integer, parameter :: BOT_COMMAND_HELP = 3 !! Return help text. - integer, parameter :: BOT_COMMAND_JID = 4 !! Return JID of bot. - integer, parameter :: BOT_COMMAND_LOG = 5 !! Return log message to logger. - integer, parameter :: BOT_COMMAND_NODE = 6 !! Return node id. - integer, parameter :: BOT_COMMAND_POKE = 7 !! Return bot response if online. - integer, parameter :: BOT_COMMAND_RECONNECT = 8 !! Reconnect. - integer, parameter :: BOT_COMMAND_UNAME = 9 !! Return system name and version. - integer, parameter :: BOT_COMMAND_UPTIME = 10 !! Return system uptime. - integer, parameter :: BOT_COMMAND_VERSION = 11 !! Return bot version. - integer, parameter :: BOT_NCOMMANDS = 11 !! Number of commands. + integer, parameter :: BOT_COMMAND_CAMERA = 2 !! Send camera image. + integer, parameter :: BOT_COMMAND_DATE = 3 !! Return date and time. + integer, parameter :: BOT_COMMAND_HELP = 4 !! Return help text. + integer, parameter :: BOT_COMMAND_JID = 5 !! Return JID of bot. + integer, parameter :: BOT_COMMAND_LOG = 6 !! Return log message to logger. + integer, parameter :: BOT_COMMAND_NODE = 7 !! Return node id. + integer, parameter :: BOT_COMMAND_POKE = 8 !! Return bot response if online. + integer, parameter :: BOT_COMMAND_RECONNECT = 9 !! Reconnect. + integer, parameter :: BOT_COMMAND_UNAME = 10 !! Return system name and version. + integer, parameter :: BOT_COMMAND_UPTIME = 11 !! Return system uptime. + integer, parameter :: BOT_COMMAND_VERSION = 12 !! Return bot version. + integer, parameter :: BOT_NCOMMANDS = 12 !! Number of commands. integer, parameter :: BOT_COMMAND_NAME_LEN = 9 !! Max. command name length. integer, parameter :: BOT_COMMAND_LEN = 1 + BOT_COMMAND_NAME_LEN !! Max. command length with prefix. @@ -40,7 +41,8 @@ program dmbot character, parameter :: BOT_COMMAND_PREFIX = '!' !! Command prefix. character(len=BOT_COMMAND_NAME_LEN), parameter :: BOT_COMMAND_NAMES(BOT_NCOMMANDS) = [ & character(len=BOT_COMMAND_NAME_LEN) :: & - 'beats', 'date', 'help', 'jid', 'log', 'node', 'poke', 'reconnect', 'uname', 'uptime', 'version' & + 'beats', 'camera', 'date', 'help', 'jid', 'log', 'node', 'poke', 'reconnect', & + 'uname', 'uptime', 'version' & ] !! Command names. type :: app_type @@ -70,6 +72,19 @@ program dmbot character(len=IM_JID_FULL_LEN), allocatable :: group(:) !! Authorised JIDs. end type bot_type + type, public :: bot_upload_type + !! HTTP upload type + character(len=FILE_PATH_LEN) :: file_path = ' ' + character(len=FILE_PATH_LEN) :: file_name = ' ' + integer(kind=i8) :: file_size = 0_i8 + character(len=IM_URL_LEN) :: url_get = ' ' + character(len=IM_URL_LEN) :: url_put = ' ' + character(len=MIME_LEN) :: content_type = ' ' + character(len=32) :: auth = ' ' + character(len=1024) :: cookie = ' ' + character(len=32) :: expires = ' ' + end type bot_upload_type + class(logger_class), pointer :: logger ! Logger object. integer :: rc ! Return code. @@ -334,17 +349,18 @@ function bot_dispatch(bot, from, message) result(reply) call logger%debug('received command ' // command // ' from ' // from) select case (c) - case (BOT_COMMAND_BEATS); output = bot_handle_beats() - case (BOT_COMMAND_DATE); output = bot_handle_date() - case (BOT_COMMAND_HELP); output = bot_handle_help() - case (BOT_COMMAND_JID); output = bot_handle_jid(bot) - case (BOT_COMMAND_LOG); output = bot_handle_log(bot, argument) - case (BOT_COMMAND_NODE); output = bot_handle_node(bot) - case (BOT_COMMAND_POKE); output = bot_handle_poke(bot) - case (BOT_COMMAND_RECONNECT); output = bot_handle_reconnect(bot) - case (BOT_COMMAND_UNAME); output = bot_handle_uname() - case (BOT_COMMAND_UPTIME); output = bot_handle_uptime() - case (BOT_COMMAND_VERSION); output = bot_handle_version() + case (BOT_COMMAND_BEATS); output = bot_response_beats() + case (BOT_COMMAND_CAMERA); output = bot_response_camera() + case (BOT_COMMAND_DATE); output = bot_response_date() + case (BOT_COMMAND_HELP); output = bot_response_help() + case (BOT_COMMAND_JID); output = bot_response_jid(bot) + case (BOT_COMMAND_LOG); output = bot_response_log(bot, argument) + case (BOT_COMMAND_NODE); output = bot_response_node(bot) + case (BOT_COMMAND_POKE); output = bot_response_poke(bot) + case (BOT_COMMAND_RECONNECT); output = bot_response_reconnect(bot) + case (BOT_COMMAND_UNAME); output = bot_response_uname() + case (BOT_COMMAND_UPTIME); output = bot_response_uptime() + case (BOT_COMMAND_VERSION); output = bot_response_version() end select reply = command // ': ' // output @@ -426,7 +442,7 @@ end function bot_parse_message ! ************************************************************************** ! BOT COMMAND HANDLING FUNCTIONS. ! ************************************************************************** - function bot_handle_beats() result(output) + function bot_response_beats() result(output) !! Returns current time in Swatch Internet Time (.beats). character(len=:), allocatable :: output !! Response string. @@ -435,16 +451,33 @@ function bot_handle_beats() result(output) rc = dm_time_to_beats(dm_time_now(), beats) output = trim(beats) - end function bot_handle_beats + end function bot_response_beats + + function bot_response_camera(bot) result(output) + !! Sends camera image. + type(bot_type), intent(inout) :: bot !! Bot type. + character(len=:), allocatable :: output !! Response string. + + character(len=:), allocatable :: content_type, file_name + character(len=ID_LEN) :: id + integer(kind=i8) :: file_size + type(c_ptr) :: iq_stanza + + output = '' + + id = dm_uuid4() + iq_stanza = dm_im_create_iq_http_upload(bot%im, id, file_name, file_size, content_type) - function bot_handle_date() result(output) + end function bot_response_camera + + function bot_response_date() result(output) !! Returns current date and time in ISO 8601. character(len=:), allocatable :: output !! Response string. output = dm_time_now() - end function bot_handle_date + end function bot_response_date - function bot_handle_help() result(output) + function bot_response_help() result(output) !! Returns help text. character(len=:), allocatable :: output !! Response string. @@ -460,17 +493,17 @@ function bot_handle_help() result(output) '!uname - return system name' // ASCII_LF // & '!uptime - return system uptime' // ASCII_LF // & '!version - return bot version' - end function bot_handle_help + end function bot_response_help - function bot_handle_jid(bot) result(output) + function bot_response_jid(bot) result(output) !! Returns full JID of bot. type(bot_type), intent(inout) :: bot !! Bot type. character(len=:), allocatable :: output !! Response string. output = '<' // trim(bot%im%jid_full) // '>' - end function bot_handle_jid + end function bot_response_jid - function bot_handle_log(bot, argument) result(output) + function bot_response_log(bot, argument) result(output) !! Sends log message to logger. type(bot_type), intent(inout) :: bot !! Bot type. character(len=*), intent(in) :: argument !! Command arguments. @@ -497,9 +530,9 @@ function bot_handle_log(bot, argument) result(output) call logger%log(lvl, message, source=bot%name) output = 'sent ' // trim(LOG_LEVEL_NAMES_LOWER(lvl)) // ' message to ' // logger%get_name() - end function bot_handle_log + end function bot_response_log - function bot_handle_node(bot) result(output) + function bot_response_node(bot) result(output) !! Returns node id. type(bot_type), intent(inout) :: bot !! Bot type. character(len=:), allocatable :: output !! Response string. @@ -513,9 +546,9 @@ function bot_handle_node(bot) result(output) else output = 'n/a' end if - end function bot_handle_node + end function bot_response_node - function bot_handle_poke(bot) result(output) + function bot_response_poke(bot) result(output) !! Returns awake message. type(bot_type), intent(inout) :: bot !! Bot type. character(len=:), allocatable :: output !! Response string. @@ -531,9 +564,9 @@ function bot_handle_poke(bot) result(output) end if output = output // ' is online' - end function bot_handle_poke + end function bot_response_poke - function bot_handle_reconnect(bot) result(output) + function bot_response_reconnect(bot) result(output) !! Reconnects bot. type(bot_type), intent(inout) :: bot !! Bot type. character(len=:), allocatable :: output !! Response string. @@ -541,9 +574,9 @@ function bot_handle_reconnect(bot) result(output) bot%reconnect = .true. call xmpp_timed_handler_add(bot%im%connection, disconnect_callback, 500_c_long, c_null_ptr) output = 'bye' - end function bot_handle_reconnect + end function bot_response_reconnect - function bot_handle_uname() result(output) + function bot_response_uname() result(output) !! Returns Unix name. character(len=:), allocatable :: output !! Response string. @@ -555,9 +588,9 @@ function bot_handle_uname() result(output) trim(uname%release) // ' ' // & trim(uname%version) // ' ' // & trim(uname%machine) - end function bot_handle_uname + end function bot_response_uname - function bot_handle_uptime() result(output) + function bot_response_uptime() result(output) !! Returns system uptime. character(len=:), allocatable :: output !! Response string. @@ -567,14 +600,67 @@ function bot_handle_uptime() result(output) call dm_system_uptime(seconds) call dm_time_delta_from_seconds(uptime, seconds) output = dm_time_delta_to_string(uptime) - end function bot_handle_uptime + end function bot_response_uptime - function bot_handle_version() result(output) + function bot_response_version() result(output) !! Returns bot version. character(len=:), allocatable :: output !! Response string. output = dm_version_to_string(APP_NAME, APP_MAJOR, APP_MINOR, APP_PATCH, library=.true.) - end function bot_handle_version + end function bot_response_version + + + subroutine http_upload(upload) + use :: curl + use :: unix + + type(bot_upload_type), intent(inout) :: upload + + integer :: stat + type(c_ptr) :: curl_ctx, list_ctx + type(c_ptr) :: fh + + fh = c_fopen(trim(upload%file_path) // c_null_char, 'rb' // c_null_char) + if (.not. c_associated(fh)) return + + stat = curl_global_init(CURL_GLOBAL_ALL) + + curl_ctx = curl_easy_init() + list_ctx = c_null_ptr + + stat = curl_easy_setopt(curl_ctx, CURLOPT_URL, trim(upload%url_put)) + stat = curl_easy_setopt(curl_ctx, CURLOPT_CUSTOMREQUEST, 'PUT') + stat = curl_easy_setopt(curl_ctx, CURLOPT_UPLOAD, 1) + stat = curl_easy_setopt(curl_ctx, CURLOPT_READDATA, fh) + stat = curl_easy_setopt(curl_ctx, CURLOPT_INFILESIZE_LARGE, upload%file_size) + + list_ctx = curl_slist_append(list_ctx, 'Content-Type: ' // trim(upload%content_type)) + + if (len_trim(upload%auth) > 0) then + list_ctx = curl_slist_append(list_ctx, 'Authorization: ' // trim(upload%auth)) + end if + + if (len_trim(upload%cookie) > 0) then + list_ctx = curl_slist_append(list_ctx, 'Cookie: ' // trim(upload%cookie)) + end if + + if (len_trim(upload%expires) > 0) then + list_ctx = curl_slist_append(list_ctx, 'Expires: ' // trim(upload%expires)) + end if + + stat = curl_easy_setopt(curl_ctx, CURLOPT_HTTPHEADER, list_ctx) + stat = curl_easy_setopt(curl_ctx, CURLOPT_NOSIGNAL, 1) + stat = curl_easy_setopt(curl_ctx, CURLOPT_VERBOSE, 0) + stat = curl_easy_setopt(curl_ctx, CURLOPT_USERAGENT, dm_version_to_string(APP_NAME, APP_MAJOR, APP_MINOR, APP_PATCH, library=.true.)) + + stat = curl_easy_perform(curl_ctx) + + call curl_slist_free_all(list_ctx) + call curl_easy_cleanup(curl_ctx) + call curl_global_cleanup() + + if (c_associated(fh)) stat = c_fclose(fh) + end subroutine http_upload ! ************************************************************************** ! CALLBACK PROCEDURES. @@ -602,8 +688,8 @@ recursive subroutine connection_callback(connection, event, error, stream_error, trim(im%host) // ':' // dm_itoa(im%port)) ! Add handlers. - call xmpp_handler_add(connection, iq_callback, '', 'iq', '', user_data) - call xmpp_handler_add(connection, message_callback, '', 'message', '', user_data) + call xmpp_handler_add(connection, ping_response_callback, IM_STANZA_NS_PING, IM_STANZA_NAME_IQ, '', user_data) + call xmpp_handler_add(connection, message_callback, '', IM_STANZA_NAME_MESSAGE, '', user_data) ! Add timed handlers. call xmpp_timed_handler_add(connection, ping_callback, int(APP_PING_INTERVAL * 1000, kind=c_long), user_data) @@ -630,58 +716,59 @@ function disconnect_callback(connection, user_data) bind(c) call xmpp_disconnect(connection) end function disconnect_callback - function iq_callback(connection, iq_stanza, user_data) bind(c) - !! C-interoperable iq stanza handler for ping processing. - type(c_ptr), intent(in), value :: connection !! xmpp_conn_t * - type(c_ptr), intent(in), value :: iq_stanza !! xmpp_stanza_t * - type(c_ptr), intent(in), value :: user_data !! void * - integer(kind=c_int) :: iq_callback !! int + function http_upload_response_callback(stanza, user_data) bind(c) + !! C-interoperable HTTP upload response callback. + type(c_ptr), intent(in), value :: stanza !! xmpp_stanza_t * + type(c_ptr), intent(in), value :: user_data !! void * + integer(kind=c_int) :: http_upload_callback !! int - character(len=:), allocatable :: from, id, type - integer :: stat - type(c_ptr) :: ping_stanza, result_stanza - type(bot_type), pointer :: bot + character(len=:), allocatable :: from, header_name, type + type(c_ptr) :: get_stanza, header_stanza, put_stanza, slot_stanza + type(bot_upload_type), pointer :: upload - iq_callback = 0 + im_http_upload_response_callback = 0 if (.not. c_associated(user_data)) return - call c_f_pointer(user_data, bot) + call c_f_pointer(user_data, upload) - ! Get stanza attributes. - from = xmpp_stanza_get_from(iq_stanza) - id = xmpp_stanza_get_id(iq_stanza) - type = xmpp_stanza_get_type(iq_stanza) + from = xmpp_stanza_get_from(stanza) + type = xmpp_stanza_get_type(stanza) - if (len(type) == 0 .or. len(id) == 0) return + if (type == IM_STANZA_TYPE_ERROR) return - select case (type) - case (IM_STANZA_TYPE_RESULT) - if (id == bot%ping_id) bot%ping_id = ' ' + slot_stanza = xmpp_stanza_get_child_by_name(stanza, IM_STANZA_NAME_SLOT) - case (IM_STANZA_TYPE_GET) - ping_stanza = xmpp_stanza_get_child_by_ns(iq_stanza, IM_STANZA_NS_PING) - - if (c_associated(ping_stanza)) then - call logger%debug('received ping from ' // from) - result_stanza = dm_im_create_iq_result(bot%im, id=id) - else - result_stanza = dm_im_create_iq_error(bot%im, id=id, type=IM_STANZA_TYPE_CANCEL, & - condition=IM_STANZA_NAME_SERVICE_UNAVAILABLE) - end if + if (xmpp_stanza_get_ns(slot_stanza) == IM_STANZA_NS_HTTP_UPLOAD) then + get_stanza = xmpp_stanza_get_child_by_name(slot_stanza, IM_STANZA_NAME_GET) + put_stanza = xmpp_stanza_get_child_by_name(slot_stanza, IM_STANZA_NAME_PUT) - stat = xmpp_stanza_set_to(result_stanza, from) - call xmpp_send(connection, result_stanza) - stat = xmpp_stanza_release(result_stanza) + if (c_associated(get_stanza) .and. c_associated(put_stanza)) then + upload%url_get = xmpp_stanza_get_attribute(get_stanza, IM_STANZA_ATTR_URL) + upload%url_put = xmpp_stanza_get_attribute(put_stanza, IM_STANZA_ATTR_URL) - case (IM_STANZA_TYPE_ERROR) - ping_stanza = xmpp_stanza_get_child_by_ns(iq_stanza, IM_STANZA_NS_PING) + header_stanza = xmpp_stanza_get_children(put_stanza) + header_stanza = xmpp_stanza_get_next(header_stanza) - if (c_associated(ping_stanza) .and. id == bot%ping_id) then - call xmpp_timed_handler_delete(connection, ping_callback) - bot%ping_id = ' ' - end if - end select - end function iq_callback + do while (c_associated(header_stanza)) + if (xmpp_stanza_get_name(header_stanza) == IM_STANZA_NAME_HEADER) then + header_name = xmpp_stanza_get_attribute(header_stanza, IM_STANZA_ATTR_NAME) + + select case (header_name) + case (IM_STANZA_HEADER_AUTHORIZATION); upload%auth = xmpp_stanza_get_text(header_stanza) + case (IM_STANZA_HEADER_COOKIE); upload%cookie = xmpp_stanza_get_text(header_stanza) + case (IM_STANZA_HEADER_EXPIRES); upload%expires = xmpp_stanza_get_text(header_stanza) + end select + end if + + header_stanza = xmpp_stanza_get_next(header_stanza) + end do + + ! start HTTP upload here ... + else + im_http_upload_response_callback = 1 + end if + end if + end function http_upload_response_callback function message_callback(connection, stanza, user_data) bind(c) !! C-interoperable message handler. Must be registered in @@ -769,6 +856,50 @@ function ping_callback(connection, user_data) bind(c) stat = xmpp_stanza_release(iq_stanza) end function ping_callback + function ping_response_callback(connection, iq_stanza, user_data) bind(c) + !! C-interoperable iq stanza handler for ping processing. + type(c_ptr), intent(in), value :: connection !! xmpp_conn_t * + type(c_ptr), intent(in), value :: iq_stanza !! xmpp_stanza_t * + type(c_ptr), intent(in), value :: user_data !! void * + integer(kind=c_int) :: iq_callback !! int + + character(len=:), allocatable :: from, id, type + integer :: stat + type(c_ptr) :: ping_stanza, result_stanza + type(bot_type), pointer :: bot + + iq_callback = 0 + + if (.not. c_associated(user_data)) return + call c_f_pointer(user_data, bot) + + ! Get stanza attributes. + from = xmpp_stanza_get_from(iq_stanza) + id = xmpp_stanza_get_id(iq_stanza) + type = xmpp_stanza_get_type(iq_stanza) + + if (len(type) == 0 .or. len(id) == 0) return + + select case (type) + case (IM_STANZA_TYPE_RESULT) + if (id == bot%ping_id) bot%ping_id = ' ' + + case (IM_STANZA_TYPE_GET) + call logger%debug('received ping from ' // from) + result_stanza = dm_im_create_iq_result(bot%im, id=id) + + stat = xmpp_stanza_set_to(result_stanza, from) + call xmpp_send(connection, result_stanza) + stat = xmpp_stanza_release(result_stanza) + + case (IM_STANZA_TYPE_ERROR) + if (id == bot%ping_id) then + call xmpp_timed_handler_delete(connection, ping_callback) + bot%ping_id = ' ' + end if + end select + end function ping_response_callback + subroutine signal_callback(signum) bind(c) !! Default POSIX signal handler of the program. integer(kind=c_int), intent(in), value :: signum !! Signal number. diff --git a/src/dm_camera.f90 b/src/dm_camera.f90 index 106b2d6..6d99343 100644 --- a/src/dm_camera.f90 +++ b/src/dm_camera.f90 @@ -27,25 +27,39 @@ module dm_camera !! ```fortran !! character(len=*), parameter :: IMAGE_PATH = '/tmp/image.jpg' !! - !! integer :: rc - !! type(camera_type) :: camera + !! integer :: rc + !! type(camera_type) :: camera + !! type(gm_text_box_type) :: text_box !! - !! camera = camera_type(input='/dev/video0', device=CAMERA_DEVICE_V4L) + !! camera = camera_type(input = '/dev/video0', & + !! device = CAMERA_DEVICE_V4L2, & + !! width = 1280, & + !! height = 720) !! !! rc = dm_camera_capture(camera, IMAGE_PATH) !! if (dm_is_error(rc)) call dm_error_out(rc) !! - !! rc = dm_gm_add_text_box(IMAGE_PATH, text=dm_time_now()) + !! text_box = gm_text_box_type(font='DroidSansMono', font_size=16) + !! rc = dm_gm_add_text_box(IMAGE_PATH, text=dm_time_now(), text_box=text_box) !! if (dm_is_error(rc)) call dm_error_out(rc) !! ``` !! - !! Change the camera type to capture an RTSP stream instead: + !! The camera must support the resolution of 1280×720 in this case. If no + !! resolution is specified, the camera default is used. Run _ffmpeg(1)_ to + !! list the supported output dimensions: + !! + !! ``` + !! $ ffmpeg -f v4l2 -list_formats all -i /dev/video0 + !! ``` + !! + !! RTSP streams are always captured in the stream resolution: !! !! ```fortran !! camera = camera_type(input='rtsp://10.10.10.15:8554/camera1', device=CAMERA_DEVICE_RTSP) !! ``` !! - !! The attribute `input` must be set to the stream URL. + !! The attribute `input` must be set to the stream URL and may include user + !! name and password. use :: dm_error use :: dm_file use :: dm_string @@ -55,13 +69,13 @@ module dm_camera ! FFmpeg devices/formats. integer, parameter, public :: CAMERA_DEVICE_NONE = 0 !! No device selected. integer, parameter, public :: CAMERA_DEVICE_RTSP = 1 !! RTSP stream. - integer, parameter, public :: CAMERA_DEVICE_V4L = 2 !! USB webcam via Video4Linux2. + integer, parameter, public :: CAMERA_DEVICE_V4L2 = 2 !! USB webcam via Video4Linux2. integer, parameter, public :: CAMERA_DEVICE_LAST = 2 !! Never use this. integer, parameter, public :: CAMERA_DEVICE_NAME_LEN = 4 character(len=*), parameter, public :: CAMERA_DEVICE_NAMES(CAMERA_DEVICE_NONE:CAMERA_DEVICE_LAST) = [ & - character(len=CAMERA_DEVICE_NAME_LEN) :: 'none', 'rtsp', 'v4l' & + character(len=CAMERA_DEVICE_NAME_LEN) :: 'none', 'rtsp', 'v4l2' & ] !! Camera device names. character(len=*), parameter :: CAMERA_BINARY = 'ffmpeg' !! FFmpeg binary name. @@ -85,7 +99,7 @@ module dm_camera ! PUBLIC PROCEDURES. ! ************************************************************************** integer function dm_camera_capture(camera, output, command) result(rc) - !! Captures a single frame from a V4L device or RTSP stream with + !! Captures a single frame from a V4L2 device or RTSP stream with !! FFmpeg, and optionally adds a timestamp with GraphicsMagick. If the !! input is an RTSP stream, the URL must start with `rtsp://`. !! @@ -138,7 +152,7 @@ pure elemental integer function dm_camera_device_from_name(name) result(device) select case (name_) case (CAMERA_DEVICE_NAMES(CAMERA_DEVICE_RTSP)); device = CAMERA_DEVICE_RTSP - case (CAMERA_DEVICE_NAMES(CAMERA_DEVICE_V4L)); device = CAMERA_DEVICE_V4L + case (CAMERA_DEVICE_NAMES(CAMERA_DEVICE_V4L2)); device = CAMERA_DEVICE_V4L2 case default; device = CAMERA_DEVICE_NONE end select end function dm_camera_device_from_name @@ -155,7 +169,7 @@ end function dm_camera_device_is_valid ! PRIVATE PROCEDURES. ! ************************************************************************** pure elemental subroutine camera_prepare_capture(command, camera, output) - !! Creates FFmpeg command to capture a single camera frame through V4L + !! Creates FFmpeg command to capture a single camera frame through V4L2 !! or RTSP. The function returns `E_INVALID` on error. character(len=CAMERA_COMMAND_LEN), intent(out) :: command !! Prepared command string. type(camera_type), intent(in) :: camera !! Camera type. @@ -172,15 +186,17 @@ pure elemental subroutine camera_prepare_capture(command, camera, output) ! overwrite output file. command = ' -i ' // trim(camera%input) // ' -f image2 -update 1 -t 0.5' // command - case (CAMERA_DEVICE_V4L) - ! Capture single frame from V4L device. + case (CAMERA_DEVICE_V4L2) + ! Capture single frame from V4L2 device. + command = ' -i ' // trim(camera%input) // ' -frames:v 1' // command + if (camera%width > 0 .and. camera%height > 0) then write (video_size, '(" -video_size ", i0, "x", i0)') camera%width, camera%height command = trim(video_size) // command end if ! Format argument `-f` must be before input argument `-i`. - command = ' -f v4l2 -i ' // trim(camera%input) // ' -frames:v 1' // command + command = ' -f v4l2' // trim(command) end select ! Concatenate command string. diff --git a/src/dm_gm.f90 b/src/dm_gm.f90 index e10c6ec..087db97 100644 --- a/src/dm_gm.f90 +++ b/src/dm_gm.f90 @@ -8,6 +8,33 @@ module dm_gm !! $ sudo apt-get install graphicsmagick !! ``` !! + !! For a list of all fonts supported by GraphicsMagick, run: + !! + !! ``` + !! $ gm convert -list font + !! Path: /usr/local/lib/GraphicsMagick/config/type-windows.mgk + !! + !! Name Family Style Stretch Weight + !! -------------------------------------------------------------------------------- + !! Arial Arial normal normal 400 + !! Arial-Black Arial normal normal 900 + !! Arial-Bold Arial normal normal 700 + !! Arial-Bold-Italic Arial italic normal 700 + !! Arial-Italic Arial italic normal 400 + !! ... + !! ``` + !! + !! Edit `/usr/local/lib/GraphicsMagick/config/type.mgk` to set a custom + !! font configuration. Use the Perl script `imagick_type_gen.pl` to + !! generate a type file `type-custom.mgk`, for example: + !! + !! ``` + !! $ find /usr/local/share/fonts/ -type f -name "*.*" | perl ./imagick_type_gen.pl -f - > type-custom.mgk + !! ``` + !! + !! Copy the type file to the `config/` directory of GraphicsMagick and + !! modify the path to the include file in `type.mgk` accordingly. + !! !! Example to read meta data of image `/tmp/image.jpg`: !! !! ```fortran @@ -31,7 +58,6 @@ module dm_gm !! ``` use :: dm_error use :: dm_file - use :: dm_mime implicit none (type, external) private @@ -108,8 +134,15 @@ module dm_gm character(len=*), parameter, public :: GM_COLOR_GRAY = 'gray' character(len=*), parameter, public :: GM_COLOR_GRAY0 = 'gray0' character(len=*), parameter, public :: GM_COLOR_GRAY1 = 'gray1' + character(len=*), parameter, public :: GM_COLOR_GRAY2 = 'gray2' + character(len=*), parameter, public :: GM_COLOR_GRAY3 = 'gray3' + character(len=*), parameter, public :: GM_COLOR_GRAY4 = 'gray4' + character(len=*), parameter, public :: GM_COLOR_GRAY5 = 'gray5' + character(len=*), parameter, public :: GM_COLOR_GRAY6 = 'gray6' + character(len=*), parameter, public :: GM_COLOR_GRAY7 = 'gray7' + character(len=*), parameter, public :: GM_COLOR_GRAY8 = 'gray8' + character(len=*), parameter, public :: GM_COLOR_GRAY9 = 'gray9' character(len=*), parameter, public :: GM_COLOR_GRAY10 = 'gray10' - character(len=*), parameter, public :: GM_COLOR_GRAY100 = 'gray100' character(len=*), parameter, public :: GM_COLOR_GRAY11 = 'gray11' character(len=*), parameter, public :: GM_COLOR_GRAY12 = 'gray12' character(len=*), parameter, public :: GM_COLOR_GRAY13 = 'gray13' @@ -119,7 +152,6 @@ module dm_gm character(len=*), parameter, public :: GM_COLOR_GRAY17 = 'gray17' character(len=*), parameter, public :: GM_COLOR_GRAY18 = 'gray18' character(len=*), parameter, public :: GM_COLOR_GRAY19 = 'gray19' - character(len=*), parameter, public :: GM_COLOR_GRAY2 = 'gray2' character(len=*), parameter, public :: GM_COLOR_GRAY20 = 'gray20' character(len=*), parameter, public :: GM_COLOR_GRAY21 = 'gray21' character(len=*), parameter, public :: GM_COLOR_GRAY22 = 'gray22' @@ -130,7 +162,6 @@ module dm_gm character(len=*), parameter, public :: GM_COLOR_GRAY27 = 'gray27' character(len=*), parameter, public :: GM_COLOR_GRAY28 = 'gray28' character(len=*), parameter, public :: GM_COLOR_GRAY29 = 'gray29' - character(len=*), parameter, public :: GM_COLOR_GRAY3 = 'gray3' character(len=*), parameter, public :: GM_COLOR_GRAY30 = 'gray30' character(len=*), parameter, public :: GM_COLOR_GRAY31 = 'gray31' character(len=*), parameter, public :: GM_COLOR_GRAY32 = 'gray32' @@ -141,7 +172,6 @@ module dm_gm character(len=*), parameter, public :: GM_COLOR_GRAY37 = 'gray37' character(len=*), parameter, public :: GM_COLOR_GRAY38 = 'gray38' character(len=*), parameter, public :: GM_COLOR_GRAY39 = 'gray39' - character(len=*), parameter, public :: GM_COLOR_GRAY4 = 'gray4' character(len=*), parameter, public :: GM_COLOR_GRAY40 = 'gray40' character(len=*), parameter, public :: GM_COLOR_GRAY41 = 'gray41' character(len=*), parameter, public :: GM_COLOR_GRAY42 = 'gray42' @@ -152,7 +182,6 @@ module dm_gm character(len=*), parameter, public :: GM_COLOR_GRAY47 = 'gray47' character(len=*), parameter, public :: GM_COLOR_GRAY48 = 'gray48' character(len=*), parameter, public :: GM_COLOR_GRAY49 = 'gray49' - character(len=*), parameter, public :: GM_COLOR_GRAY5 = 'gray5' character(len=*), parameter, public :: GM_COLOR_GRAY50 = 'gray50' character(len=*), parameter, public :: GM_COLOR_GRAY51 = 'gray51' character(len=*), parameter, public :: GM_COLOR_GRAY52 = 'gray52' @@ -163,7 +192,6 @@ module dm_gm character(len=*), parameter, public :: GM_COLOR_GRAY57 = 'gray57' character(len=*), parameter, public :: GM_COLOR_GRAY58 = 'gray58' character(len=*), parameter, public :: GM_COLOR_GRAY59 = 'gray59' - character(len=*), parameter, public :: GM_COLOR_GRAY6 = 'gray6' character(len=*), parameter, public :: GM_COLOR_GRAY60 = 'gray60' character(len=*), parameter, public :: GM_COLOR_GRAY61 = 'gray61' character(len=*), parameter, public :: GM_COLOR_GRAY62 = 'gray62' @@ -174,7 +202,6 @@ module dm_gm character(len=*), parameter, public :: GM_COLOR_GRAY67 = 'gray67' character(len=*), parameter, public :: GM_COLOR_GRAY68 = 'gray68' character(len=*), parameter, public :: GM_COLOR_GRAY69 = 'gray69' - character(len=*), parameter, public :: GM_COLOR_GRAY7 = 'gray7' character(len=*), parameter, public :: GM_COLOR_GRAY70 = 'gray70' character(len=*), parameter, public :: GM_COLOR_GRAY71 = 'gray71' character(len=*), parameter, public :: GM_COLOR_GRAY72 = 'gray72' @@ -185,7 +212,6 @@ module dm_gm character(len=*), parameter, public :: GM_COLOR_GRAY77 = 'gray77' character(len=*), parameter, public :: GM_COLOR_GRAY78 = 'gray78' character(len=*), parameter, public :: GM_COLOR_GRAY79 = 'gray79' - character(len=*), parameter, public :: GM_COLOR_GRAY8 = 'gray8' character(len=*), parameter, public :: GM_COLOR_GRAY80 = 'gray80' character(len=*), parameter, public :: GM_COLOR_GRAY81 = 'gray81' character(len=*), parameter, public :: GM_COLOR_GRAY82 = 'gray82' @@ -196,7 +222,6 @@ module dm_gm character(len=*), parameter, public :: GM_COLOR_GRAY87 = 'gray87' character(len=*), parameter, public :: GM_COLOR_GRAY88 = 'gray88' character(len=*), parameter, public :: GM_COLOR_GRAY89 = 'gray89' - character(len=*), parameter, public :: GM_COLOR_GRAY9 = 'gray9' character(len=*), parameter, public :: GM_COLOR_GRAY90 = 'gray90' character(len=*), parameter, public :: GM_COLOR_GRAY91 = 'gray91' character(len=*), parameter, public :: GM_COLOR_GRAY92 = 'gray92' @@ -207,6 +232,7 @@ module dm_gm character(len=*), parameter, public :: GM_COLOR_GRAY97 = 'gray97' character(len=*), parameter, public :: GM_COLOR_GRAY98 = 'gray98' character(len=*), parameter, public :: GM_COLOR_GRAY99 = 'gray99' + character(len=*), parameter, public :: GM_COLOR_GRAY100 = 'gray100' character(len=*), parameter, public :: GM_COLOR_GREEN = 'green' character(len=*), parameter, public :: GM_COLOR_GREEN_YELLOW = 'greenyellow' character(len=*), parameter, public :: GM_COLOR_GREY = 'grey' @@ -305,7 +331,7 @@ module dm_gm character(len=*), parameter :: GM_BINARY = 'gm' !! GraphicsMagick binary name. type, public :: gm_text_box_type - !! Text box settings for drawing text onto camera frame image. + !! Text box settings for drawing text on image. !! !! You can tag a font to specify whether it is a PostScript, TrueType, !! or X11 font. For example, `Arial.ttf` is a TrueType font, @@ -313,7 +339,7 @@ module dm_gm character(len=GM_GRAVITY_LEN) :: gravity = GM_GRAVITY_SW !! Text position (GM). character(len=GM_COLOR_LEN) :: background = GM_COLOR_BLACK !! Box colour (GM). character(len=GM_COLOR_LEN) :: foreground = GM_COLOR_WHITE !! Text colour (GM). - character(len=GM_FONT_LEN) :: font = 'Arial' !! Font name (GM). + character(len=GM_FONT_LEN) :: font = 'DejaVuSansMono' !! Font name (GM). integer :: font_size = 12 !! Font size in points. end type gm_text_box_type @@ -333,26 +359,8 @@ module dm_gm ! ************************************************************************** integer function dm_gm_add_text_box(path, text, text_box, command) result(rc) !! Draws text camera image file, using GraphicsMagick. By default, the - !! text box is drawn to the bottom-left corner of the image. - !! - !! For a list of all supported font names, run: - !! - !! ``` - !! $ gm convert -list font - !! Path: /usr/local/lib/GraphicsMagick/config/type-windows.mgk - !! - !! Name Family Style Stretch Weight - !! -------------------------------------------------------------------------------- - !! Arial Arial normal normal 400 - !! Arial-Black Arial normal normal 900 - !! Arial-Bold Arial normal normal 700 - !! Arial-Bold-Italic Arial italic normal 700 - !! Arial-Italic Arial italic normal 400 - !! ... - !! ``` - !! - !! If no text box is passed, the default values of the derived type are - !! used. + !! text box is drawn to the bottom-left corner of the image. If no text + !! box is passed, the default values of the derived type are used. !! !! The function returns the following error codes: !! @@ -506,6 +514,8 @@ integer function dm_gm_get_mime(path, mime) result(rc) !! * `E_READ` if reading dimensions failed. !! * `E_SYSTEM` if execution of GraphicsMagick failed. !! + use :: dm_mime + character(len=*), intent(in) :: path !! Image file path. character(len=:), allocatable, intent(out) :: mime !! MIME type. diff --git a/src/dm_im.f90 b/src/dm_im.f90 index 852f0af..f3ada0c 100644 --- a/src/dm_im.f90 +++ b/src/dm_im.f90 @@ -72,8 +72,6 @@ module dm_im use :: dm_id use :: dm_kind use :: dm_mime - use :: dm_sem - use :: dm_uuid implicit none (type, external) private @@ -284,21 +282,6 @@ module dm_im character(len=IM_PASSWORD_LEN) :: password = ' ' !! XMPP password of account. end type im_type - type, public :: im_upload_type - !! IM HTTP upload context type - character(len=FILE_PATH_LEN) :: file_path = ' ' - character(len=FILE_PATH_LEN) :: file_name = ' ' - integer(kind=i8) :: file_size = 0_i8 - character(len=IM_URL_LEN) :: url_get = ' ' - character(len=IM_URL_LEN) :: url_put = ' ' - character(len=MIME_LEN) :: content_type = ' ' - character(len=32) :: auth = ' ' - character(len=1024) :: cookie = ' ' - character(len=32) :: expires = ' ' - integer :: error = E_NONE - character(len=512) :: error_message = ' ' - end type im_upload_type - ! Imported abstract interfaces. public :: dm_im_callback public :: dm_im_certfail_callback @@ -328,9 +311,6 @@ module dm_im ! Private procedures. private :: im_stanza_get_error_message - - ! Private callbacks. - private :: im_http_upload_response_callback contains ! ************************************************************************** ! PUBLIC FUNCTIONS. @@ -346,17 +326,17 @@ integer function dm_im_connect(im, host, port, jid, password, callback, user_dat !! * `E_NULL` if the XMPP context is not associated. !! * `E_XMPP` if a connection context could not be created. !! - type(im_type), intent(inout) :: im !! IM context type. - character(len=*), intent(in) :: host !! XMPP server (IP address or FQDN). - integer, intent(in) :: port !! XMPP server port. - character(len=*), intent(in) :: jid !! IM ID (JID). - character(len=*), intent(in) :: password !! JID account password. - procedure(dm_im_connection_callback) :: callback !! IM connection handler. - type(c_ptr), intent(in), optional :: user_data !! C pointer to user data. - character(len=*), intent(in), optional :: resource !! Optional resource (`@/`). - logical, intent(in), optional :: keep_alive !! Enable TCP Keep Alive. - logical, intent(in), optional :: tls_required !! TLS is mandatory. - logical, intent(in), optional :: tls_trusted !! Trust TLS certificate. + type(im_type), intent(inout) :: im !! IM context type. + character(len=*), intent(in) :: host !! XMPP server (IP address or FQDN). + integer, intent(in) :: port !! XMPP server port. + character(len=*), intent(in) :: jid !! IM ID (JID). + character(len=*), intent(in) :: password !! JID account password. + procedure(dm_im_connection_callback) :: callback !! IM connection handler. + type(c_ptr), intent(in), optional :: user_data !! C pointer to user data. + character(len=*), intent(in), optional :: resource !! Optional resource (`@/`). + logical, intent(in), optional :: keep_alive !! Enable TCP Keep Alive. + logical, intent(in), optional :: tls_required !! TLS is mandatory. + logical, intent(in), optional :: tls_trusted !! Trust TLS certificate. integer :: stat integer(kind=c_long) :: flags @@ -691,67 +671,4 @@ function im_stanza_get_error_message(stanza) result(message) if (.not. allocated(message)) message = 'unknown' end function im_stanza_get_error_message - - ! ************************************************************************** - ! PRIVATE CALLBACKS. - ! ************************************************************************** - function im_http_upload_response_callback(stanza, user_data) bind(c) - !! C-interoperable HTTP upload response callback. - type(c_ptr), intent(in), value :: stanza !! xmpp_stanza_t * - type(c_ptr), intent(in), value :: user_data !! void * - integer(kind=c_int) :: im_http_upload_response_callback !! int - - character(len=:), allocatable :: from, header_name, type - type(c_ptr) :: get_stanza, header_stanza, put_stanza, slot_stanza - type(im_upload_type), pointer :: upload - - im_http_upload_response_callback = 0 - - if (.not. c_associated(user_data)) return - call c_f_pointer(user_data, upload) - - from = xmpp_stanza_get_from(stanza) - type = xmpp_stanza_get_type(stanza) - - if (type == IM_STANZA_TYPE_ERROR) then - upload%error = E_IO - upload%error_message = im_stanza_get_error_message(stanza) - return - end if - - slot_stanza = xmpp_stanza_get_child_by_name(stanza, IM_STANZA_NAME_SLOT) - - if (xmpp_stanza_get_ns(slot_stanza) == IM_STANZA_NS_HTTP_UPLOAD) then - get_stanza = xmpp_stanza_get_child_by_name(slot_stanza, IM_STANZA_NAME_GET) - put_stanza = xmpp_stanza_get_child_by_name(slot_stanza, IM_STANZA_NAME_PUT) - - if (c_associated(get_stanza) .and. c_associated(put_stanza)) then - upload%url_get = xmpp_stanza_get_attribute(get_stanza, IM_STANZA_ATTR_URL) - upload%url_put = xmpp_stanza_get_attribute(put_stanza, IM_STANZA_ATTR_URL) - - header_stanza = xmpp_stanza_get_children(put_stanza) - header_stanza = xmpp_stanza_get_next(header_stanza) - - do while (c_associated(header_stanza)) - if (xmpp_stanza_get_name(header_stanza) == IM_STANZA_NAME_HEADER) then - header_name = xmpp_stanza_get_attribute(header_stanza, IM_STANZA_ATTR_NAME) - - select case (header_name) - case (IM_STANZA_HEADER_AUTHORIZATION); upload%auth = xmpp_stanza_get_text(header_stanza) - case (IM_STANZA_HEADER_COOKIE); upload%cookie = xmpp_stanza_get_text(header_stanza) - case (IM_STANZA_HEADER_EXPIRES); upload%expires = xmpp_stanza_get_text(header_stanza) - case default; upload%error = E_TYPE - end select - end if - - header_stanza = xmpp_stanza_get_next(header_stanza) - end do - - ! start HTTP upload here ... - else - upload%error = E_INVALID - im_http_upload_response_callback = 1 - end if - end if - end function im_http_upload_response_callback end module dm_im