From bf340a5dafa5d4108f6f0b1271fe78a89d7d545a Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Tue, 12 Jun 2018 15:38:21 +0200 Subject: [PATCH] Migrated memes to own file, and added reload functionallity. --- internet.lua | 1 + main.lua | 549 +++------------------------------------------------ memes.lua | 537 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 566 insertions(+), 521 deletions(-) create mode 100644 memes.lua diff --git a/internet.lua b/internet.lua index 6c66996..7d8769f 100644 --- a/internet.lua +++ b/internet.lua @@ -1,6 +1,7 @@ local https = require 'ssl.https' local md5 = require 'md5' +local json = require 'json' local internet = {} diff --git a/main.lua b/main.lua index 089186e..46e1d19 100644 --- a/main.lua +++ b/main.lua @@ -28,11 +28,12 @@ local MEME_CHANCE = 0.3 -- Make sure all required modules can be loaded local imlib = require 'imlib2' +for _, path in ipairs(CONFIG.IMGGEN_PATH_FONTS) do + imlib.font.add_path(path) +end require 'socket' -local json = require 'json' - require 'irc.init' local sleep = require 'socket'.sleep @@ -42,411 +43,10 @@ local signal do if a then signal = b end end -local internet = require 'internet' +local memes = require 'memes' -------------------------------------------------------------------------------- --- Meme utils - -for _, path in ipairs(CONFIG.IMGGEN_PATH_FONTS) do - imlib.font.add_path(path) -end - -local function flatten_onto(target_img, other_img, x0, y0) - assert(x0 + other_img:get_width() <= target_img:get_width()) - assert(y0 + other_img:get_height() <= target_img:get_height()) - - for x = 0, other_img:get_width() do - for y = 0, other_img:get_height() do - target_img:draw_pixel(x + x0, y + y0, other_img:get_pixel(x, y)) - end - end - -- - return -end - -local function clean_text (text) - return text:gsub('%s+', ' '):match('^%s*(.-)%s*$') -end - - --------------------------------------------------------------------------------- --- Meme creations - -local function generate_bait_link() - return 'https://dcav.pw/jbait' -end - -local function fit_font_to_line_height (font_name, height) - assert(type(font_name) == 'string') - assert(type(height) == 'number') - -- - local size_min, size_max = 1, 50 - while true do - local size_guess = math.floor((size_max + size_min)/2) - assert(size_min <= size_guess and size_guess <= size_max) - local font = assert(imlib.font.load(font_name..'/'..size_guess)) - if size_guess == size_min then return font, size_guess end - local text_w, text_h = font:get_size 'k' - -- - if text_h <= height then size_min = size_guess - else size_max = size_guess - end - end - return nil -end - -local function load_font_of_size (font_name, size) - return assert(imlib.font.load(font_name..'/'..size)) -end - -local function determine_required_font_size (font_name, text, width) - assert(type(font_name) == 'string') - assert(type(text) == 'string') - assert(type(width) == 'number') - -- - local size_min, size_max = 1, 50 - while true do - local size_guess = math.floor((size_max + size_min)/2) - assert(size_min <= size_guess and size_guess <= size_max) - local font = load_font_of_size(font_name, size_guess) - if size_guess == size_min then return font, size_guess end - local text_w, text_h = font:get_size(text) - -- - if text_w <= width then size_min = size_guess - else size_max = size_guess - end - end - return nil -end - -local function wrap_lines (font, text, width) - local lines, current, x = {}, {}, 0 - local SPACE_ADVANCE = font:get_advance ' ' - for word in text:gmatch '[^%s]+' do - assert(type(word) == 'string') - local word_w = font:get_advance(word) - local next_x = x + word_w + SPACE_ADVANCE - if word == '' then - -- Do nothing - elseif next_x <= width then - current[#current+1], x = word, next_x - else - lines[#lines+1], current = table.concat(current, ' '), { word } - x = word_w - end - end - -- Færdiggør sidste linje - lines[#lines+1] = table.concat(current, ' ') - -- Ret - return lines -end - -local COLOR_WHITE = imlib.color.new(255, 255, 255) -local COLOR_BLACK = imlib.color.new( 0, 0, 0) - -local function draw_centered_lines (draw_onto, lines, x0, y0, width, font, font_color) - assert(type(lines) == 'table') - local y = y0 - for i, line in ipairs(lines) do - local text_w, text_h = font:get_size(line) - local x = x0 + (width - text_w) / 2 - draw_onto:draw_text(font, line, x, y, font_color or COLOR_BLACK) - -- - y = y + text_h - end -end - -local function determine_font_and_lines_for_box (font_name, text, width, height) - local num_lines, last_actual, actual_lines = 1, nil, nil - for iteration = 1, 100 do - local estimate_font = determine_required_font_size(font_name, text, width * num_lines) - local line_height = select(2, estimate_font:get_size(text)) - - local estimate_num_lines = height / select(2, estimate_font:get_size(text)) - local lines = wrap_lines(estimate_font, text, width) - local actual_num_lines = #lines - if #lines * line_height >= height then - estimate_num_lines = estimate_num_lines - 1 - end - -- Test for convergence - if last_actual == actual_num_lines then - actual_lines = lines - break - end - num_lines = (estimate_num_lines + actual_num_lines) / 2 - last_actual = actual_num_lines - end - - assert(actual_lines) - -- Find final font size - local _, min_font_size = fit_font_to_line_height(font_name, height / #actual_lines) - for _, line in ipairs(actual_lines) do - min_font_size = math.min(min_font_size, select(2, determine_required_font_size(font_name, line, width))) - end - local actual_font = load_font_of_size(font_name, min_font_size) - - -- Assertions and error correction - if (#actual_lines * select(2, actual_font:get_size(text)) < height) then - io.write(' !! Performed bad fitting of text: '..text..'\n') - actual_font, actual_lines = determine_required_font_size(font_name, text, width), {text} - end - - return actual_font, actual_lines -end - -local function draw_centered_text_in_box (font_name, draw_onto, text, x0, y0, width, height, bg_color, font_color) - assert(type(font_name) == 'string') - local font, lines = determine_font_and_lines_for_box(font_name, text, width, height) - -- - local y = y0 + (height - #lines * select(2, font:get_size(text))) / 2 - -- - if bg_color then draw_onto:fill_rectangle(x0, y0, width, height, bg_color) end - draw_centered_lines(draw_onto, lines, x0, y, width, font, font_color) -end - -local function choose_random_font () - local fonts = imlib.font.list_fonts() - assert(#fonts > 0, 'Could not find any fonts') - return fonts[math.random(#fonts)] -end - -local function load_random_font () - local font_name = choose_random_font() - local font, errmsg = imlib.font.load(font_name..'/30') - assert(font, errmsg) - return font_name -end - -local DROSTE_EFFECT_TRIGGERS = { - ['induktion'] = true, - ['rekursion'] = true, - ['uendelig rekursion'] = true, - ['rekursive'] = true, - ['rekursive funktioner'] = true, - ['recursion'] = true, - ['infinite recursion'] = true, - ['tail recursion'] = true, - ['recursive'] = true, - ['recursive function'] = true, - ['induktion'] = true, - ['droste'] = true, - ['drostle'] = true, -- Misspelling - - ['uendelig'] = true, - ['uendeligt'] = true, - - ['strange loop'] = true, - ['loop'] = true, -} - -local DROSTE_ITERATIONS_DEFAULT = 5 - -local function fill_in_topics_information (topics) - assert(type(topics) == 'table') - -- - local topic_to_image_url = internet.search_images(topics) - -- - local new_topics = {} - for i, topic in ipairs(topics) do - assert(type(topic) == 'string') - - local url = topic_to_image_url[topic] - - if DROSTE_EFFECT_TRIGGERS[topic] then new_topics[i] = { topic = topic, type = 'droste' } - elseif url then new_topics[i] = { topic = topic, type = 'image', url = url } - else new_topics[i] = { topic = topic, type = 'text', text = topic } - end - end - return new_topics -end - -local function paste_topic_onto_image (target, topic, x, y, w, h, bg_color, font_name, font_color) - assert(target) - assert(type(topic) == 'table') - assert(type(font_name) == 'string') - -- Download and paste found image - if topic.type == 'image' then - local file_extension = topic.url:match '%.(%a+)$' - local url, filename = topic.url, CONFIG.IMGGEN_PATH_OUTPUT..'topic_'..topic.topic..'.'..file_extension - assert(type(url) == 'string' and #url > 0) - assert(type(filename) == 'string' and #filename > 0) - internet.download_file(url, filename) - -- Convert svg to png - if url:match '%.svg$' then - local filename_2 = CONFIG.IMGGEN_PATH_OUTPUT..'topic_'..topic.topic..'.'..'png' - os.execute('convert -density "1200" -resize 400x400 "'..filename..'" "'..filename_2..'"') - filename = filename_2 - end - -- - local found_img = assert(imlib.image.load(filename)) - found_img:crop_and_scale(0, 0, found_img:get_width(), found_img:get_height(), w, h) - flatten_onto (target, found_img, x, y) - found_img:free() - --os.remove(filename) - elseif topic.type == 'text' then - local text = topic.text - draw_centered_text_in_box(font_name, target, text, x, y, w, h, bg_color, font_color) - elseif topic.type == 'droste' then - target:fill_rectangle(x, y, w, h, COLOR_WHITE) - else - assert(false, topic.type) - end -end - -local function copy_remotely (origin_server, origin_path, target_server, target_path) - local origin = origin_server and origin_server ~= 'localhost' and origin_server..':'..origin_path or origin_path - local target = target_server and target_server ~= 'localhost' and target_server..':'..target_path or target_path - os.execute('scp '..origin..' '..target..' > /dev/null') -end - -local function save_to_cloud (img) - assert(img) - -- - local MEME_OUTPUT = CONFIG.IMGGEN_PATH_OUTPUT..'meme.png' - img:save (MEME_OUTPUT) - img:free() - -- Upload to dcav - local img_name = 'otmemes_'..os.time()..'.png' - copy_remotely('localhost', MEME_OUTPUT, CONFIG.STORAGE_SERVER, CONFIG.STORAGE_SERVER_PATH..img_name) - return CONFIG.STORAGE_DIR_URL..img_name -end - - -local function draw_droste_effect (target_image, droste_positions, iterations) - iterations = interations or DROSTE_ITERATIONS_DEFAULT - if #droste_positions <= 0 or iterations <= 0 then return target_image end - for _ = 1, iterations do - for _, position in ipairs(droste_positions) do - local paste_image = target_image:clone() - paste_image:crop_and_scale(0, 0, paste_image:get_width(), paste_image:get_height(), position.w, position.h) - flatten_onto (target_image, paste_image, position.x, position.y) - paste_image:free() - end - end - return target_image -end - -local function droste_positions_from_topics (topics, places) - local droste_poss = {} - for i, topic in ipairs(topics) do - if topic.type == 'droste' then - droste_poss[#droste_poss+1] = { - x = places[i].x, y = places[i].y, - w = places[i].w, h = places[i].h, - } - end - end - return droste_poss -end - -local function shallow_copy (t0) - local t = {} - for k,v in pairs(t0) do t[k] = v end - return t -end - -local function generate_comparison_meme_generator (positions) - assert(type(positions) == 'table') - assert(type(positions.base_img_path) == 'string') - - return function (topics) - assert(type(topics) == 'table' and #topics == #positions) - - local font_name = choose_random_font() - - local base_img = imlib.image.load(positions.base_img_path) - - -- Randomize pos - local rand_positions = {} - for i, pos in ipairs(positions) do - rand_positions[i] = shallow_copy(pos) - rand_positions[i].x = pos.x + math.random(-(pos.xr or 0), pos.xr or 0) - rand_positions[i].y = pos.y + math.random(-(pos.yr or 0), pos.yr or 0) - end - - -- Paste topic onto head - for index, pos in ipairs(rand_positions) do - paste_topic_onto_image(base_img, topics[index], pos.x, pos.y, pos.w, pos.h, nil, font_name, pos.font_color) - end - - -- Droste - base_img = draw_droste_effect(base_img, droste_positions_from_topics(topics, rand_positions)) - - -- - return save_to_cloud(base_img) - end -end - -local generate_distracted_boyfriend = generate_comparison_meme_generator { - base_img_path = './images/distracted_boyfriend.jpg', - - { x = 640, xr = 20, y = 50, yr = 20, w = 150, h = 150 }, - { x = 180, xr = 20, y = 50, yr = 20, w = 150, h = 150 }, -} - -local generate_distracted_boyfriend_oldy_times = generate_comparison_meme_generator { - base_img_path = './images/distracted_boyfriend_oldy_times.jpg', - - { x = 650, xr = 10, y = 100, yr = 20, w = 150, h = 150 }, - { x = 124, xr = 10, y = 38, yr = 10, w = 250, h = 250 }, -} - -local generate_drake_egon_olsen = generate_comparison_meme_generator { - base_img_path = './images/drake_egon.png', - - { x = 380, xr = 0, y = 0, yr = 0, w = 380, h = 354 }, - { x = 377, xr = 0, y = 354, yr = 0, w = 383, h = 360 }, -} - -local generate_skorsten_image = generate_comparison_meme_generator { - base_img_path = './images/bamse_skorsten_1.png', - - { x = 646, xr = 0, y = 366, yr = 0, w = 631, h = 215, font_color = COLOR_WHITE }, -} - -local GENERATE_COMPARISON_MEME_OF_2 = { - generate_distracted_boyfriend, - generate_distracted_boyfriend_oldy_times, - generate_drake_egon_olsen -} - -local BRAIN_ROW_HEIGHT = 150 -local BRAIN_ROW_WIDTH = 200 - -local BRAIN_MAX_EXPLOSION_TOPICS = 6 -local BRAIN_DROSTE_POS = {} -for i = 1, BRAIN_MAX_EXPLOSION_TOPICS do - BRAIN_DROSTE_POS[i] = { - x = 0, - y = (i-1) * BRAIN_ROW_HEIGHT, - w = BRAIN_ROW_WIDTH, - h = BRAIN_ROW_HEIGHT - } -end - - -local function generate_brain_explosion_image (topics) - assert(type(topics) == 'table' and 1 <= #topics and #topics <= BRAIN_MAX_EXPLOSION_TOPICS) - -- - local base_img = imlib.image.new(BRAIN_ROW_WIDTH * 2, BRAIN_ROW_HEIGHT * #topics) - base_img:fill_rectangle(0, 0, BRAIN_ROW_WIDTH * 2, BRAIN_ROW_HEIGHT * #topics, COLOR_WHITE) - local font_name = choose_random_font() - -- - for i, topic_info in ipairs(topics) do - paste_topic_onto_image(base_img, topic_info, 0, (i-1) * BRAIN_ROW_HEIGHT, BRAIN_ROW_WIDTH, BRAIN_ROW_HEIGHT, COLOR_WHITE, font_name) - - local brain_img = imlib.image.load(CONFIG.IMGGEN_PATH_BRAINS..'brain_'..i..'.png') - brain_img:crop_and_scale(0, 0, brain_img:get_width(), brain_img:get_height(), BRAIN_ROW_WIDTH, BRAIN_ROW_HEIGHT) - flatten_onto (base_img, brain_img, BRAIN_ROW_WIDTH, (i-1) * BRAIN_ROW_HEIGHT) - brain_img:free() - end - - -- Droste - base_img = draw_droste_effect(base_img, droste_positions_from_topics(topics, BRAIN_DROSTE_POS)) - - -- Save - return save_to_cloud(base_img) -end +-- Util local function choose(l) assert(type(l) == 'table') @@ -483,84 +83,19 @@ end local LAST_ACTIVE = os.time() local ACTIVE_PERIOD = 10 -local function human_delay () +local function human_delay (start_meme_time) local delay = { 0.5, 0.5 } -- Not active if LAST_ACTIVE + ACTIVE_PERIOD < os.time() then delay = { 3, 3 } end -- Sleep - sleep(math.random() * delay[2] + delay[1]) + local sleep_until = (start_meme_time or os.time()) + math.random() * delay[2] + delay[1] + sleep(sleep_until - os.time()) -- Set last active LAST_ACTIVE = os.time() end -local function is_comparison_message (message) - local leq, gep = message:match '<', message:match '>' - return leq and not gep and '<' or not leq and gep and '>' -end - -local function reverse (l0) - local l, j = {}, #l0 - for i = 1, #l0 do - l[i], j = l0[j], j - 1 - end - return l -end - -local COMP_SENTENCES do - local ORD_MED_ATTITYDE = { - bedre = 'positiv', - dårligere = 'negativ', - hurtigere = 'positiv', - langsommere = 'negativt', - robust = 'positivt', - } - COMP_SENTENCES = {} - for ord, attityde in pairs(ORD_MED_ATTITYDE) do - local first, second = '{A}', '{B}' - if attityde == 'negativ' then first, second = second, first end - for _, fmt in ipairs { '%s er %s end %s', '%s er mere %s end %s', '%s er meget %s end %s', '%s %s end %s' } do - table.insert(COMP_SENTENCES, fmt:format(first, ord, second)) - end - end -end - -for sentence_i, sentence in ipairs(COMP_SENTENCES) do - local A_pos, B_pos = sentence:find '{A}', sentence:find '{B}' - local best_first = (A_pos < B_pos) - local pattern_sentence = sentence:gsub('{[AB]}', '(.+)'):gsub('%s+','%%s+') - COMP_SENTENCES[pattern_sentence] = { best_first = best_first } - COMP_SENTENCES[sentence_i] = nil -end - -local function get_topics_from_comparison_message(message) - local comp = is_comparison_message(message) - if comp then - local topics = {} - for topic in message:gmatch('[^'..comp..']+') do - local topic_text = clean_text(topic) - if topic_text:match '[^%s]' then - topics[#topics+1] = topic_text - end - end - -- Rev - if comp == '>' then topics = reverse(topics) end - -- - return (#topics >= 2) and topics or nil - end - - -- Natural language comparisons - for pattern, sentence_info in pairs(COMP_SENTENCES) do - local good, bad = message:lower():match(pattern) - if not sentence_info.best_first then good, bad = bad, good end - if good then return { bad, good } end - end - - -- - return nil -end - local function common_error(channel, text, ...) local errmsg = text:format(...) bot:sendChat(channel, errmsg) @@ -695,55 +230,15 @@ local function handle_message(bot, user, channel, message, is_fast_channel) return 'LUCK' end - -- Bait msg - if message:match '%f[%a]bait%f[%A]' then - human_delay() - bot:sendChat(channel, generate_bait_link()) + local start_meme_time = os.time() + + local reply, status = memes.generate_for_message(user, message) + if reply then + human_delay(start_meme_time) + bot:sendChat(channel, reply) FORRIGE_MEME = os.time() - return 'BAIT' + return status end - - -- Meme machine - if message:match '%f[%a]meme%s+machine%f[%A]' then - human_delay() - bot:sendChat(channel, 'https://www.youtube.com/watch?v=wl-LeTFM8zo') - return 'MACHINE' - end - - -- Vælter min skorsten - do - local problem_text = message:lower():match 'vælter%s+min%s+skorsten%s*(.+)$' - if problem_text then - if problem_text:match '^er' then problem_text = problem_text:match '^er%s*(.+)$' end - if problem_text:match '^:' then problem_text = problem_text:match '^:%s*(.+)$' end - local img_link = generate_skorsten_image {{ type = 'text', text = problem_text }} - bot:sendChat(channel, img_link) - return 'SKRSTEN' - end - end - - -- Comparison memes - local topics = get_topics_from_comparison_message(message) - if not topics then return end - - local topics = fill_in_topics_information(topics) - - local img_link, error_message - -- - if #topics == 2 then - img_link, error_message = choose(GENERATE_COMPARISON_MEME_OF_2)(topics) - else - assert(#topics >= 3) - img_link, error_message = generate_brain_explosion_image(topics) - end - -- - if img_link then - bot:sendChat(channel, img_link) - else - bot:sendChat(channel, 'Sorry, no can do. '..tostring(error_message)..'.') - end - FORRIGE_MEME = os.time() - return 'COMPAR' end local DEBUG_CHANNEL = '#bot-test' @@ -832,10 +327,22 @@ local function init_memebot () if signal then signal.signal(signal.SIGINT, function (signum) io.write ' !! Received SIGINT, will shut down...\n' - --send_to_all(BOT_FAREWELL[math.random(#BOT_FAREWELL)]) + send_to_all(BOT_FAREWELL[math.random(#BOT_FAREWELL)]) shutdown_memebot() os.exit(128 + signum) end) + signal.signal(signal.SIGUSR1, function (signum) + io.write ' !! Received SIGUSR1, will reload modules...\n' + for _, module_name in ipairs { 'internet', 'memes' } do + local old_module = require(module_name) + package.loaded[module_name] = nil + local new_module = require(module_name) + for k, v in pairs(new_module) do + old_module[k] = v + end + io.write(' - '..module_name..'\n') + end + end) else io.write ' !! Module "posix.signal" was missing, so CTRL+C will hard-crash.\n' end diff --git a/memes.lua b/memes.lua new file mode 100644 index 0000000..3da744a --- /dev/null +++ b/memes.lua @@ -0,0 +1,537 @@ + +local memes = {} + +local internet = require 'internet' +local imlib = require 'imlib2' +local CONFIG = require 'config' + +-------------------------------------------------------------------------------- +-- Util + +local function reverse (l0) + local l, j = {}, #l0 + for i = 1, #l0 do + l[i], j = l0[j], j - 1 + end + return l +end + +local function choose(l) + assert(type(l) == 'table') + return l[math.random(#l)] +end + +local function shallow_copy (t0) + local t = {} + for k,v in pairs(t0) do t[k] = v end + return t +end + +local function clean_text (text) + return text:gsub('%s+', ' '):match('^%s*(.-)%s*$') +end + + +-------------------------------------------------------------------------------- +-- Internet shit + +local function copy_remotely (origin_server, origin_path, target_server, target_path) + local origin = origin_server and origin_server ~= 'localhost' and origin_server..':'..origin_path or origin_path + local target = target_server and target_server ~= 'localhost' and target_server..':'..target_path or target_path + os.execute('scp '..origin..' '..target..' > /dev/null') +end + +local function save_to_cloud (img) + assert(img) + -- + local MEME_OUTPUT = CONFIG.IMGGEN_PATH_OUTPUT..'meme.png' + img:save (MEME_OUTPUT) + img:free() + -- Upload to dcav + local img_name = 'otmemes_'..os.time()..'.png' + copy_remotely('localhost', MEME_OUTPUT, CONFIG.STORAGE_SERVER, CONFIG.STORAGE_SERVER_PATH..img_name) + return CONFIG.STORAGE_DIR_URL..img_name +end + +-------------------------------------------------------------------------------- +-- Draw shit + +local function flatten_onto(target_img, other_img, x0, y0) + assert(x0 + other_img:get_width() <= target_img:get_width()) + assert(y0 + other_img:get_height() <= target_img:get_height()) + + for x = 0, other_img:get_width() do + for y = 0, other_img:get_height() do + target_img:draw_pixel(x + x0, y + y0, other_img:get_pixel(x, y)) + end + end + -- + return +end + +local function fit_font_to_line_height (font_name, height) + assert(type(font_name) == 'string') + assert(type(height) == 'number') + -- + local size_min, size_max = 1, 50 + while true do + local size_guess = math.floor((size_max + size_min)/2) + assert(size_min <= size_guess and size_guess <= size_max) + local font = assert(imlib.font.load(font_name..'/'..size_guess)) + if size_guess == size_min then return font, size_guess end + local text_w, text_h = font:get_size 'k' + -- + if text_h <= height then size_min = size_guess + else size_max = size_guess + end + end + return nil +end + +local function load_font_of_size (font_name, size) + return assert(imlib.font.load(font_name..'/'..size)) +end + +local function determine_required_font_size (font_name, text, width) + assert(type(font_name) == 'string') + assert(type(text) == 'string') + assert(type(width) == 'number') + -- + local size_min, size_max = 1, 50 + while true do + local size_guess = math.floor((size_max + size_min)/2) + assert(size_min <= size_guess and size_guess <= size_max) + local font = load_font_of_size(font_name, size_guess) + if size_guess == size_min then return font, size_guess end + local text_w, text_h = font:get_size(text) + -- + if text_w <= width then size_min = size_guess + else size_max = size_guess + end + end + return nil +end + +local function wrap_lines (font, text, width) + local lines, current, x = {}, {}, 0 + local SPACE_ADVANCE = font:get_advance ' ' + for word in text:gmatch '[^%s]+' do + assert(type(word) == 'string') + local word_w = font:get_advance(word) + local next_x = x + word_w + SPACE_ADVANCE + if word == '' then + -- Do nothing + elseif next_x <= width then + current[#current+1], x = word, next_x + else + lines[#lines+1], current = table.concat(current, ' '), { word } + x = word_w + end + end + -- Færdiggør sidste linje + lines[#lines+1] = table.concat(current, ' ') + -- Ret + return lines +end + +local COLOR_WHITE = imlib.color.new(255, 255, 255) +local COLOR_BLACK = imlib.color.new( 0, 0, 0) + +local function draw_centered_lines (draw_onto, lines, x0, y0, width, font, font_color) + assert(type(lines) == 'table') + local y = y0 + for i, line in ipairs(lines) do + local text_w, text_h = font:get_size(line) + local x = x0 + (width - text_w) / 2 + draw_onto:draw_text(font, line, x, y, font_color or COLOR_BLACK) + -- + y = y + text_h + end +end + +local function determine_font_and_lines_for_box (font_name, text, width, height) + local num_lines, last_actual, actual_lines = 1, nil, nil + for iteration = 1, 100 do + local estimate_font = determine_required_font_size(font_name, text, width * num_lines) + local line_height = select(2, estimate_font:get_size(text)) + + local estimate_num_lines = height / select(2, estimate_font:get_size(text)) + local lines = wrap_lines(estimate_font, text, width) + local actual_num_lines = #lines + if #lines * line_height >= height then + estimate_num_lines = estimate_num_lines - 1 + end + -- Test for convergence + if last_actual == actual_num_lines then + actual_lines = lines + break + end + num_lines = (estimate_num_lines + actual_num_lines) / 2 + last_actual = actual_num_lines + end + + assert(actual_lines) + -- Find final font size + local _, min_font_size = fit_font_to_line_height(font_name, height / #actual_lines) + for _, line in ipairs(actual_lines) do + min_font_size = math.min(min_font_size, select(2, determine_required_font_size(font_name, line, width))) + end + local actual_font = load_font_of_size(font_name, min_font_size) + + -- Assertions and error correction + if (#actual_lines * select(2, actual_font:get_size(text)) < height) then + io.write(' !! Performed bad fitting of text: '..text..'\n') + actual_font, actual_lines = determine_required_font_size(font_name, text, width), {text} + end + + return actual_font, actual_lines +end + +local function draw_centered_text_in_box (font_name, draw_onto, text, x0, y0, width, height, bg_color, font_color) + assert(type(font_name) == 'string') + local font, lines = determine_font_and_lines_for_box(font_name, text, width, height) + -- + local y = y0 + (height - #lines * select(2, font:get_size(text))) / 2 + -- + if bg_color then draw_onto:fill_rectangle(x0, y0, width, height, bg_color) end + draw_centered_lines(draw_onto, lines, x0, y, width, font, font_color) +end + +local function choose_random_font () + local fonts = imlib.font.list_fonts() + assert(#fonts > 0, 'Could not find any fonts') + return fonts[math.random(#fonts)] +end + +local function load_random_font () + local font_name = choose_random_font() + local font, errmsg = imlib.font.load(font_name..'/30') + assert(font, errmsg) + return font_name +end + +local function paste_topic_onto_image (target, topic, x, y, w, h, bg_color, font_name, font_color) + assert(target) + assert(type(topic) == 'table') + assert(type(font_name) == 'string') + -- Download and paste found image + if topic.type == 'image' then + local file_extension = topic.url:match '%.(%a+)$' + local url, filename = topic.url, CONFIG.IMGGEN_PATH_OUTPUT..'topic_'..topic.topic..'.'..file_extension + assert(type(url) == 'string' and #url > 0) + assert(type(filename) == 'string' and #filename > 0) + internet.download_file(url, filename) + -- Convert svg to png + if url:match '%.svg$' then + local filename_2 = CONFIG.IMGGEN_PATH_OUTPUT..'topic_'..topic.topic..'.'..'png' + os.execute('convert -density "1200" -resize 400x400 "'..filename..'" "'..filename_2..'"') + filename = filename_2 + end + -- + local found_img = assert(imlib.image.load(filename)) + found_img:crop_and_scale(0, 0, found_img:get_width(), found_img:get_height(), w, h) + flatten_onto (target, found_img, x, y) + found_img:free() + --os.remove(filename) + elseif topic.type == 'text' then + local text = topic.text + draw_centered_text_in_box(font_name, target, text, x, y, w, h, bg_color, font_color) + elseif topic.type == 'droste' then + target:fill_rectangle(x, y, w, h, COLOR_WHITE) + else + assert(false, topic.type) + end +end + +local DROSTE_ITERATIONS_DEFAULT = 10 + +local function draw_droste_effect (target_image, droste_positions, iterations) + assert(type(droste_positions) == 'table') + iterations = interations or DROSTE_ITERATIONS_DEFAULT + assert(type(iterations) == 'number') + -- + if #droste_positions <= 0 or iterations <= 0 then return target_image end + for _ = 1, iterations do + for _, position in ipairs(droste_positions) do + local paste_image = target_image:clone() + paste_image:crop_and_scale(0, 0, paste_image:get_width(), paste_image:get_height(), position.w, position.h) + flatten_onto (target_image, paste_image, position.x, position.y) + paste_image:free() + end + end + return target_image +end + +-------------------------------------------------------------------------------- +-- Comparison memes + + + +local DROSTE_EFFECT_TRIGGERS = { + ['induktion'] = true, + ['rekursion'] = true, + ['uendelig rekursion'] = true, + ['rekursive'] = true, + ['rekursive funktioner'] = true, + ['recursion'] = true, + ['infinite recursion'] = true, + ['tail recursion'] = true, + ['recursive'] = true, + ['recursive function'] = true, + ['induktion'] = true, + ['droste'] = true, + ['drostle'] = true, -- Misspelling + + ['uendelig'] = true, + ['uendeligt'] = true, + + ['strange loop'] = true, + ['loop'] = true, +} + +local function fill_in_topics_information (topics) + assert(type(topics) == 'table') + -- + local topic_to_image_url = internet.search_images(topics) + -- + local new_topics = {} + for i, topic in ipairs(topics) do + assert(type(topic) == 'string') + + local url = topic_to_image_url[topic] + + if DROSTE_EFFECT_TRIGGERS[topic] then new_topics[i] = { topic = topic, type = 'droste' } + elseif url then new_topics[i] = { topic = topic, type = 'image', url = url } + else new_topics[i] = { topic = topic, type = 'text', text = topic } + end + end + return new_topics +end + + + +local function droste_positions_from_topics (topics, places) + local droste_poss = {} + for i, topic in ipairs(topics) do + if topic.type == 'droste' then + droste_poss[#droste_poss+1] = { + x = places[i].x, y = places[i].y, + w = places[i].w, h = places[i].h, + } + end + end + return droste_poss +end + +local function generate_comparison_meme_generator (positions) + assert(type(positions) == 'table') + assert(type(positions.base_img_path) == 'string') + + return function (topics) + assert(type(topics) == 'table' and #topics == #positions) + + local font_name = choose_random_font() + + local base_img = imlib.image.load(positions.base_img_path) + + -- Randomize pos + local rand_positions = {} + for i, pos in ipairs(positions) do + rand_positions[i] = shallow_copy(pos) + rand_positions[i].x = pos.x + math.random(-(pos.xr or 0), pos.xr or 0) + rand_positions[i].y = pos.y + math.random(-(pos.yr or 0), pos.yr or 0) + end + + -- Paste topic onto head + for index, pos in ipairs(rand_positions) do + paste_topic_onto_image(base_img, topics[index], pos.x, pos.y, pos.w, pos.h, nil, font_name, pos.font_color) + end + + -- Droste + base_img = draw_droste_effect(base_img, droste_positions_from_topics(topics, rand_positions)) + + -- + return save_to_cloud(base_img) + end +end + +local generate_distracted_boyfriend = generate_comparison_meme_generator { + base_img_path = './images/distracted_boyfriend.jpg', + + { x = 640, xr = 20, y = 50, yr = 20, w = 150, h = 150 }, + { x = 180, xr = 20, y = 50, yr = 20, w = 150, h = 150 }, +} + +local generate_distracted_boyfriend_oldy_times = generate_comparison_meme_generator { + base_img_path = './images/distracted_boyfriend_oldy_times.jpg', + + { x = 650, xr = 10, y = 100, yr = 20, w = 150, h = 150, font_color = COLOR_WHITE }, + { x = 124, xr = 10, y = 38, yr = 10, w = 250, h = 250, font_color = COLOR_WHITE }, +} + +local generate_drake_egon_olsen = generate_comparison_meme_generator { + base_img_path = './images/drake_egon.png', + + { x = 380, xr = 0, y = 0, yr = 0, w = 380, h = 354 }, + { x = 377, xr = 0, y = 354, yr = 0, w = 383, h = 360 }, +} + +local generate_skorsten_image = generate_comparison_meme_generator { + base_img_path = './images/bamse_skorsten_1.png', + + { x = 646, xr = 0, y = 366, yr = 0, w = 631, h = 215, font_color = COLOR_WHITE }, +} + +local GENERATE_COMPARISON_MEME_OF_2 = { + generate_distracted_boyfriend, + generate_distracted_boyfriend_oldy_times, + generate_drake_egon_olsen +} + +local BRAIN_ROW_HEIGHT = 150 +local BRAIN_ROW_WIDTH = 200 + +local BRAIN_MAX_EXPLOSION_TOPICS = 6 +local BRAIN_DROSTE_POS = {} +for i = 1, BRAIN_MAX_EXPLOSION_TOPICS do + BRAIN_DROSTE_POS[i] = { + x = 0, + y = (i-1) * BRAIN_ROW_HEIGHT, + w = BRAIN_ROW_WIDTH, + h = BRAIN_ROW_HEIGHT + } +end + + +local function generate_brain_explosion_image (topics) + assert(type(topics) == 'table' and 1 <= #topics and #topics <= BRAIN_MAX_EXPLOSION_TOPICS) + -- + local base_img = imlib.image.new(BRAIN_ROW_WIDTH * 2, BRAIN_ROW_HEIGHT * #topics) + base_img:fill_rectangle(0, 0, BRAIN_ROW_WIDTH * 2, BRAIN_ROW_HEIGHT * #topics, COLOR_WHITE) + local font_name = choose_random_font() + -- + for i, topic_info in ipairs(topics) do + paste_topic_onto_image(base_img, topic_info, 0, (i-1) * BRAIN_ROW_HEIGHT, BRAIN_ROW_WIDTH, BRAIN_ROW_HEIGHT, COLOR_WHITE, font_name) + + local brain_img = imlib.image.load(CONFIG.IMGGEN_PATH_BRAINS..'brain_'..i..'.png') + brain_img:crop_and_scale(0, 0, brain_img:get_width(), brain_img:get_height(), BRAIN_ROW_WIDTH, BRAIN_ROW_HEIGHT) + flatten_onto (base_img, brain_img, BRAIN_ROW_WIDTH, (i-1) * BRAIN_ROW_HEIGHT) + brain_img:free() + end + + -- Droste + base_img = draw_droste_effect(base_img, droste_positions_from_topics(topics, BRAIN_DROSTE_POS)) + + -- Save + return save_to_cloud(base_img) +end + + + +local function is_comparison_message (message) + local leq, gep = message:match '<', message:match '>' + return leq and not gep and '<' or not leq and gep and '>' +end + +local COMP_SENTENCES do + local ORD_MED_ATTITYDE = { + bedre = 'positiv', + dårligere = 'negativ', + hurtigere = 'positiv', + langsommere = 'negativt', + robust = 'positivt', + } + COMP_SENTENCES = {} + for ord, attityde in pairs(ORD_MED_ATTITYDE) do + local first, second = '{A}', '{B}' + if attityde == 'negativ' then first, second = second, first end + for _, fmt in ipairs { '%s er %s end %s', '%s er mere %s end %s', '%s er meget %s end %s', '%s %s end %s' } do + table.insert(COMP_SENTENCES, fmt:format(first, ord, second)) + end + end +end + +for sentence_i, sentence in ipairs(COMP_SENTENCES) do + local A_pos, B_pos = sentence:find '{A}', sentence:find '{B}' + local best_first = (A_pos < B_pos) + local pattern_sentence = sentence:gsub('{[AB]}', '(.+)'):gsub('%s+','%%s+') + COMP_SENTENCES[pattern_sentence] = { best_first = best_first } + COMP_SENTENCES[sentence_i] = nil +end + +local function get_topics_from_comparison_message(message) + local comp = is_comparison_message(message) + if comp then + local topics = {} + for topic in message:gmatch('[^'..comp..']+') do + local topic_text = clean_text(topic) + if topic_text:match '[^%s]' then + topics[#topics+1] = topic_text + end + end + -- Rev + if comp == '>' then topics = reverse(topics) end + -- + return (#topics >= 2) and topics or nil + end + + -- Natural language comparisons + for pattern, sentence_info in pairs(COMP_SENTENCES) do + local good, bad = message:lower():match(pattern) + if not sentence_info.best_first then good, bad = bad, good end + if good then return { bad, good } end + end + + -- + return nil +end + +-------------------------------------------------------------------------------- +-- Other memes + +local function generate_bait_link() + return 'https://dcav.pw/jbait' +end + +-------------------------------------------------------------------------------- + +function memes.generate_for_message (user, message) + + -- Bait msg + if message:match '%f[%a]bait%f[%A]' then + return generate_bait_link(), 'BAIT' + end + + -- Meme machine + if message:match '%f[%a]meme%s+machine%f[%A]' then + return 'https://www.youtube.com/watch?v=wl-LeTFM8zo', 'MACHINE' + end + + -- Vælter min skorsten + do + local problem_text = message:lower():match 'vælter%s+min%s+skorsten%s*(.+)$' + if problem_text then + if problem_text:match '^er' then problem_text = problem_text:match '^er%s*(.+)$' end + if problem_text:match '^:' then problem_text = problem_text:match '^:%s*(.+)$' end + local img_link = generate_skorsten_image {{ type = 'text', text = problem_text }} + return img_link, 'SKRSTEN' + end + end + + -- Comparison memes + local topics = get_topics_from_comparison_message(message) + if not topics then return end + + local topics = fill_in_topics_information(topics) + -- + assert(#topics >= 2) + local img_link = (#topics == 2) and choose(GENERATE_COMPARISON_MEME_OF_2)(topics) + or generate_brain_explosion_image(topics) + -- + return img_link, 'COMPAR' +end + +-------------------------------------------------------------------------------- + +return memes +