[matex.lua] 🖼️ Используем иконки/материалы по ссылке вместо контента

Моя библиотека, которая создает материал по ссылке на изображение.

Мой “велосипед” был создан, как замена библиотеке texture.lua от SuperiorServers.

Причина, по которой САПовский texture.lua перестал меня устраивать в его довольно неповоротном для использования API и баге, который кешировал “изображение” даже если вместо изображения сайт с картинкой вернул ошибку (404 not found, 429 rate limit reached, капча, доступ запрещен, ошибка сервера).


:information_source: Использование (API)

Всего 2 основные функции:

matex.download(url_to_image, callback)


Самая базовая функция. После скачивания изображения, материал передается в callback функцию.

Пример:
(при открытии интерфейса скачивает изображение и потом в Paint его рисует)

function PANEL:Init()
	matex.download("https://i.imgur.com/TZcJ1CK.png", function(mat)
		self.mat = mat
		print("У нас скачался материал!!!")
		do_another_actions()
	end)
end

function PANEL:Paint(w, h)
	if self.mat then
		surface.SetDrawColor(color_white)
		surface.SetMaterial(self.mat)
		surface.DrawTexturedRect(0, 0, w, h)
	end
end

matex.now(url_to_image)


Позволяет использовать материал в HUDPaint, PANEL:Paint (и т.д.) хуках, словно он УЖЕ скачан (даже если это не так). Используется маленький трюк. Это самый удобный вариант использования в большинстве случаев.

Пример:

hook.Add("HUDPaint", "matex_demo", function()
	local logo_mat = matex.now("https://i.imgur.com/TZcJ1CK.png")
	if logo_mat then
		surface.SetDrawColor(color_white)
		surface.SetMaterial(logo_mat)
		surface.DrawTexturedRect(35, 35, 570, 460)
	end
end)

:arrow_down: Скачать matex.lua

:exclamation: Если вы не знаете, куда этот файл разместить, значит он не для вас

-- TRIGON.IM 12 dec 2021
-- Упрощенная версия texture либы от dash
-- 2024.12.27 dec 2024 добавлена проверка is_normal_image, чтобы всякие 429 и 403 от imgur не кешировали говно

matex = matex or {}

file.CreateDir("matex")

local PNG_START = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}
local PNG_TRAIL = {0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82}
local is_png = function(raw)
	for i = 1, 8 do
		if PNG_START[i] ~= string.byte(raw, i) then return false end
		if PNG_TRAIL[i] ~= string.byte(raw, -(9 - i)) then return false end
	end
	return true
end


local JPG_START = {0xFF, 0xD8, 0xFF}
local JPG_TRAIL = {0xFF, 0xD9}
local is_jpg = function(raw)
	for i = 1, 3 do
		if JPG_START[i] ~= string.byte(raw, i) then return false end
		if i == 3 then break end
		if JPG_TRAIL[i] ~= string.byte(raw, -(3 - i)) then return false end
	end
	return true
end


-- https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
-- https://www.garykessler.net/library/file_sigs.html
local function is_normal_image(raw)
	local png, jpg = is_png(raw), is_jpg(raw)
	return png or jpg
end

function matex.download(url, callback, useproxy)
	local id = util.CRC(url)

	local filepath = "matex/" .. id .. ".png"
	local matpath  = "../data/matex/" .. id .. ".png"

	if file.Exists(filepath, "DATA") then
		callback( Material(matpath, "noclamp smooth") )
		return
	end

	local baseurl = useproxy and "https://proxy.duckduckgo.com/iu/?u=" .. url or url
	http.Fetch(baseurl, function(body)
		if is_normal_image(body) then file.Write(filepath, body) end
		callback( Material(matpath, "noclamp smooth") )
	end, function()
		if useproxy then callback( Material("nil") ) return end
		matex.download(url, callback, true)
	end)
end

local cache = {}
function matex.now(url)
	if cache[url] then return cache[url].material end
	cache[url] = {material = nil}
	matex.download(url, function(material) cache[url].material = material end)
	return cache[url].material
end
3 лайка

Библиотека огонь если вы используете очень много пнг картинок и не хотите их грузить в воркшоп.

Небольшой лайфхак :
Если вы ставите картинку которую загрузили, обязательно перезагружайте интерфейс.
Например, в некоторых таб меню интерфейс не пересоздается, а скрывается что ведёт к тому, что картинка не будет кэширована после загрузки.

что это значит? :thinking:

и то, что ты дальше написал, то тоже не очень понятно

Иконки кэшированые до скачивания через эту библиотеку будут показывать фиолетовую текстуру.

эта библиотека отдает готовый материал, а не фиолетовый полуфабрикат. Пока материал полностью не скачается – в коллбек ничего не вернется. Фиолетовый материал может получиться только если пытаться загрузить всякие .bmp, .svg и прочие не-png/jpg форматы

ох ты ж, прикольдесно, помню написал сам для ся пару лет назад подобное, но вышло не так… сложно что ли?

код
local bruh = Material('path/to/loading.png', 'noclamp smooth')
function util_DownloadMaterial(url)
    if cache[url] and cache[url].failed == true then return bruh end
	if cache[url] and cache[url].downloading == true then return bruh end
	if cache[url] then return cache[url].material end
	local name = string.Explode('/', url)
	name = name[#name]
	if not file.IsDir('webmats/', 'DATA') then file.CreateDir('webmats/') end
	cache[url] = {}
	if file.Exists('webmats/' .. name, 'DATA') then
		cache[url].material = Material('../data/webmats/' .. name, 'noclamp smooth')
	else
		cache[url].downloading = true
		http.Fetch(url, function(data)
			if isstring(data) and string.StartWith(data, '<!doctype html') then
				print('Cannot load material: ' .. url)
				cache[url].failed = true
				cache[url].downloading = false
				return bruh
			end
			file.Write('webmats/' .. name, data)
			cache[url].material = Material('../data/webmats/' .. name, 'noclamp smooth')
			cache[url].downloading = false
		end)
	end
	return cache[url].material
end

используется как твоя now, но без доп. проверки, т.к. подменяет текстуру на “иконку загрузки” в виде bruh на время скачивания (+ меньше лишних строк кода выходит)

пример худпейнт
hook.Add('HUDPaint', 'виьетка епта', function()
	surface.SetDrawColor(255,255,255)
	surface.SetMaterial(util_DownloadMaterial('/виньетка.png'))
	surface.DrawTexturedRect(0,0,ScrW(),ScrH())
end)

получается ещё более лёгкая функция с гипер простой логикой.
делал для своей же файлопомойки и не проводил стресс-тесты, так что не знаю как поведёт себя со ссылками с имгура или дискорда, но пока никто из игроков не жаловался на ошибки или отсутствие текстур.

а че скинул то - я очень люблю анализировать чужой код, чтобы посмотреть на разные подходы к задачам и уверен, что не я один такой.
мне крайне понравилось как ты решил вычислять именно пнг и жпг, когда я пошёл от обратного - проверял доступна ли страница и/или является ошибкой (страницы в большинстве случаев начинаются с <!doctype html).

так что было интересно увидеть решение к которому пришёл ты, хорошее решение, спасибо за релиз!

1 лайк

Вот сюда вставь print() ради интереса и засунь функцию в HUDPaint/Think и подобное. Будет сюрприз

Еще я так и не понял в каком месте эта функция «проще». В проверке на картинку? Ну вставь туда ссылку на .gif/.exe и посмотри что будет.

Я тоже мог создать молоток, в который встроен фонарик. Но понимаешь… не всем нужен фонарик в молотке.

Некоторые хотят забивать гвозди без фонарика. А некоторые хотят чтобы фонарик был налобным.

Разработка это про гибкость. В твоем решении ты навязал бы разработчикам фонарик, даже если у них уже есть свой или если он им не нужен. И одновременно украл у них возможность использовать фонарик другого цвета/яркости/с_мерцаниями, не наделав костылей.

Подобный функционал называется расширением и в моем случае ничего не мешает без вмешивания в основную функцию прицепить фонарик к молотку

local default = Material("models/effects/portalrift_sheet")

function matex.now_with_fallback(url_to_image)
	return matex.now(url_to_image) or default
end

Если в моем решении заменить проверку is_normal_image на такую же проверку, как у тебя, то количество строк кода у нас примерно сравняется. Только цена за уменьшение количества строк кода таким образом получается слишком высока. Потому что если по какой-то причине будет кеширован мусорный результат, то игрок уже по сути никогда не увидит реальной картинки.

Держи. Меньше строк кода ценой вероятности навсегда кешировать мусор.

local matex = {}

file.CreateDir("matex")

function matex.download(url, callback, useproxy)
	local id = util.CRC(url)

	local filepath = "matex/" .. id .. ".png"
	local matpath  = "../data/matex/" .. id .. ".png"

	if file.Exists(filepath, "DATA") then
		callback( Material(matpath, "noclamp smooth") )
		return
	end

	local baseurl = useproxy and "https://proxy.duckduckgo.com/iu/?u=" .. url or url
	http.Fetch(baseurl, function(body)
		if not body:match("^<!doctype html") then file.Write(filepath, body) end
		callback( Material(matpath, "noclamp smooth") )
	end, function()
		if useproxy then callback( Material("nil") ) return end
		matex.download(url, callback, true)
	end)
end

function matex.url(url)
	local def = {material = nil}
	matex.download(url, function(material) def.material = material end)
	return def
end

local cache = {}
function matex.now(url)
	if cache[url] then return cache[url].material end
	cache[url] = matex.url(url)
end

А вот еще меньше строк кода, но теперь уже:

  • ценой получить вечную загрузку без коллбека, потому что удален error callback в http.Fetch (как у тебя)
  • ценой отсутствия повтора загрузки изображения через proxy (как у тебя), а это было очень актуально с тем же имгуром, потому что у некоторых имгур картинки напрямую не открываются из-за региональных и прочих ограничений
local matex = {}

file.CreateDir("matex")

function matex.download(url, callback)
	local id = util.CRC(url)

	local filepath = "matex/" .. id .. ".png"
	local matpath  = "../data/matex/" .. id .. ".png"

	if file.Exists(filepath, "DATA") then
		callback( Material(matpath, "noclamp smooth") )
		return
	end

	http.Fetch(url, function(body)
		if not body:StartWith("<!doctype html") then file.Write(filepath, body) end
		callback( Material(matpath, "noclamp smooth") )
	end)
end

function matex.url(url)
	local def = {material = nil}
	matex.download(url, function(material) def.material = material end)
	return def
end

local cache = {}
function matex.now(url)
	if cache[url] then return cache[url].material end
	cache[url] = matex.url(url)
end

Но даже на этом этапе моя либа остается более функциональной, хотя бы потому что есть возможность получить callback при скачивании изображения и легко ее расширять нужным функционалом с минимумом усилий

И вишенка на торте. Вот решение, которое ПОЛНОСТЬЮ повторяет функционал твоего, только:

  • кода меньше
  • код более читаемый
  • бага, как у тебя с повторными вызовами не случится

При этом не рекомендую его использовать из-за вероятности не получить нужную картинку (например с имгура из-за региональных ограничений или рейт лимитов) или кешировать мусор и навсегда вместо нормального материала смотреть на розовую текстурку. Кроме того, вы не сможете получить коллбек, когда изображение скачается без лишних костылей

local matex, cache = {}, {}

file.CreateDir("matex")

function matex.url(url)
	if cache[url] then return cache[url].material end

	local def = {material = nil}
	cache[url] = def

	local id = util.CRC(url)

	local filepath = "matex/" .. id .. ".png"
	local matpath  = "../data/matex/" .. id .. ".png"

	if file.Exists(filepath, "DATA") then
		def.material = Material(matpath, "noclamp smooth")
		return def
	end

	http.Fetch(url, function(body)
		if not body:StartWith("<!doctype html") then file.Write(filepath, body) end
		def.material = Material(matpath, "noclamp smooth")
	end)

	return def
end

Визуальное сравнение моего и твоего решения

Обновил либу. Пока писал сообщения сверху, то осознал, что функция matex.url это просто лишний сахар и удалил ее. Вместо нее можно обойтись .download и .now во всех (найденных мною) случаях без визуальной порчи кода.

ага, редко вижу кто из под луа байтики файликов трогает, тому и считаю что он “сложнее” в понимании для начинающего пользователя эй пи ай гарис мод луа

не-не-не, ты немного не прав!
специально ж уточнил, что делалось исключительно для себя в конкретных условиях, а так же что целью было показать что можно подойти к задаче иначе: стараться найти не сам пнг/жпг, а понять является ли страница страницей, а не файлом через <!doctype

т.е. не говорил что мой метод лучше и не призывал к использованию, а наоборот хвалил твой как альтернативу моему, ведь метод достижения цели у нас разный.

твой метод более крутой благодаря проксям и обнаруживанию файлов содержащих пиксели (мой же и .mp3шку скачает если ему её дать, но я не стану ж этого делать)

я только сейчас глянул и был удивлён как много ты дал рецензии на моё решение, и очень рад что это ещё сподвигло к небольшому рекфактору твоего (без троллинга, правда)