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 local function check_file_exists (filename) assert(type(filename) == 'string') local status = os.execute('test -e "'..filename..'"') return status == 0 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 local cmd = (origin_server == 'localhost' and target_server == 'localhost') and 'cp' or 'scp' -- if origin_server == 'localhost' and not check_file_exists(origin_path) then error('File "'..origin_path..'" does not exist!') end -- local status = os.execute(cmd..' '..origin..' '..target) if status ~= 0 then error('Could not copy file! Got error code: '..tostring(status)) end end local function save_file_to_dcav (filename) assert(type(filename) == 'string') -- Upload to dcav local ext = filename:match '%.(%a+)$' local remote_name = 'otmemes_'..os.time()..'.'..ext copy_remotely('localhost', filename, CONFIG.STORAGE_SERVER, CONFIG.STORAGE_SERVER_PATH..remote_name) return CONFIG.STORAGE_DIR_URL..remote_name end local function save_img_to_cloud (img) assert(img) -- local filename = CONFIG.IMGGEN_PATH_OUTPUT..'meme.png' img:save (filename) img:free() return save_file_to_dcav(filename) 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 MAX_FONT_SIZE = 1000 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, MAX_FONT_SIZE 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, MAX_FONT_SIZE 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_colors) assert(type(lines) == 'table') local y = y0 for i, line in ipairs(lines) do local text_w, text_h = font:get_size(line) local SHADOW_OFFSET = select(2, font:get_size(line)) / -20 local x = x0 + (width - text_w) / 2 for i, color in ipairs(type(font_colors) == 'table' and font_colors or {font_colors}) do local offset = (i-1) * SHADOW_OFFSET draw_onto:draw_text(font, line, x + offset, y + offset, color) end -- 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_colors) 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_colors) 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, os.tmpname() --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..'" &> /dev/null') 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 local font_colors = {} if pos.font_color == COLOR_WHITE then font_colors[1] = COLOR_BLACK elseif pos.font_color == COLOR_BLACK then font_colors[1] = COLOR_WHITE end table.insert(font_colors, pos.font_color) paste_topic_onto_image(base_img, topics[index], pos.x, pos.y, pos.w, pos.h, nil, font_name, font_colors) end -- Droste base_img = draw_droste_effect(base_img, droste_positions_from_topics(topics, rand_positions)) -- return save_img_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_into_the_trash = generate_comparison_meme_generator { base_img_path = './images/into_the_trash.png', { x = 377, xr = 20, y = 261, yr = 20, w = 400, h = 400, font_color = COLOR_BLACK }, } local generate_is_this_a_pidgeon = generate_comparison_meme_generator { base_img_path = './images/is_this_a_pidgeon.png', { x = 1100, xr = 20, y = 100, yr = 20, w = 450, h = 450, font_color = COLOR_WHITE }, { x = 15, xr = 0, y = 1200, yr = 0, w = 1587 - 15, h = 1443-1200-15, 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_img_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 local curb = require 'curb_your_enthusiasm' -------------------------------------------------------------------------------- local function is_image_link (str) print(str) if type(str) ~= 'string' then return false end if not str:match '^%a+://.+$' then return false end local headers = internet.download_headers(str) print(headers) return type(headers) == 'table' and headers['content-type']:match '^image/%a+$' 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 do -- Curb your enthusiasm local url = message:match '^[Cc]urb%s+[Yy]our%s+(.+)$' if url then local url2, timestamp = message:match '^(..-)%s+at%s+(-?[%d.]+)$' if url2 then url = url2 end timestamp = timestamp and tonumber(timestamp) or nil assert(type(timestamp) == 'number' or timestamp == nil) local video_filename = curb.your_video(url, timestamp) assert(check_file_exists(video_filename)) local video_url = save_file_to_dcav(video_filename) os.remove(video_filename) return video_url, 'CURB' end 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 -- Into the trash do -- Attempt to match local problem_text for _, pattern in ipairs { '^(.-)%s+[Ee][Rr]%s+[Ss][Kk][Rr][Aa][Ll][Dd](.-)$', '^(.-)%s+[Ii][Ss]%s+[Tt][Rr][Aa][Ss][Hh](.-)$' } do problem_text = message:match(pattern) if problem_text then break end end -- Create image based on match if problem_text then local topic = fill_in_topics_information { problem_text } local img_link = generate_into_the_trash(topic) return img_link, 'TRASH' end end -- Is this a rich picture? if is_image_link(message) then local url = message local img_link = generate_is_this_a_pidgeon { { type = 'image', url = url }, { type = 'text', text = 'Is this a rich picture?' } } return img_link, 'KYNG' 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