-------------------------------------------- OCIF Image Format ----------------------------------------------------------- local copyright = [[ Автор: Pirnogion VK: https://vk.com/id88323331 Соавтор: IT VK: https://vk.com/id7799889 ]] --------------------------------------- Подгрузка библиотек -------------------------------------------------------------- local bit = require("bit32") -- Адаптивная загрузка необходимых библиотек и компонентов local libraries = { ["component"] = "component", ["unicode"] = "unicode", ["fs"] = "filesystem", ["colorlib"] = "colorlib", } local components = { ["gpu"] = "gpu", } for library in pairs(libraries) do if not _G[library] then _G[library] = require(libraries[library]) end end for comp in pairs(components) do if not _G[comp] then _G[comp] = _G.component[components[comp]] end end libraries, components = nil, nil local image = {} -------------------------------------------- Переменные ------------------------------------------------------------------- --Cигнатура OCIF-файла local ocif_signature1 = 0x896F6369 local ocif_signature2 = 0x00661A0A --7 bytes: 89 6F 63 69 66 1A 0A local ocif_signature_expand = { string.char(0x89), string.char(0x6F), string.char(0x63), string.char(0x69), string.char(0x66), string.char(0x1A), string.char(0x0A) } --Константы программы local constants = { elementCount = 4, byteSize = 8, nullChar = 0, rawImageLoadStep = 19, fileOpenError = "Can't open file", compressedFileFormat = ".pic", rawFileFormat = ".rawpic", pngFileFormat = ".png", } ---------------------------------------- Локальные функции ------------------------------------------------------------------- --Выделить бит-терминатор в первом байте UTF-8 символа: 1100 0010 --> 0010 0000 local function selectTerminateBit_l() local prevByte = nil local prevTerminateBit = nil return function( byte ) local x, terminateBit = nil if ( prevByte == byte ) then return prevTerminateBit end x = bit32.band( bit32.bnot(byte), 0x000000FF ) x = bit32.bor( x, bit32.rshift(x, 1) ) x = bit32.bor( x, bit32.rshift(x, 2) ) x = bit32.bor( x, bit32.rshift(x, 4) ) x = bit32.bor( x, bit32.rshift(x, 8) ) x = bit32.bor( x, bit32.rshift(x, 16) ) terminateBit = x - bit32.rshift(x, 1) prevByte = byte prevTerminateBit = terminateBit return terminateBit end end local selectTerminateBit = selectTerminateBit_l() --Прочитать n байтов из файла, возвращает прочитанные байты как число, если не удалось прочитать, то возвращает 0 local function readBytes(file, bytes) local readedByte = 0 local readedNumber = 0 for i = bytes, 1, -1 do readedByte = string.byte( file:read(1) or constants.nullChar ) readedNumber = readedNumber + bit32.lshift(readedByte, i * constants.byteSize - constants.byteSize) end return readedNumber end --Подготавливает цвета и символ для записи в файл сжатого формата local function encodePixel(background, foreground, alpha, char) --Расхерачиваем жирные цвета в компактные цвета local ascii_background1, ascii_background2, ascii_background3 = colorlib.HEXtoRGB(background) local ascii_foreground1, ascii_foreground2, ascii_foreground3 = colorlib.HEXtoRGB(foreground) --Расхерачиваем жирный код юникод-символа в несколько миленьких ascii-кодов local ascii_char1, ascii_char2, ascii_char3, ascii_char4, ascii_char5, ascii_char6 = string.byte( char, 1, 6 ) ascii_char1 = ascii_char1 or constants.nullChar --Возвращаем все расхераченное return ascii_background1, ascii_background2, ascii_background3, ascii_foreground1, ascii_foreground2, ascii_foreground3, alpha, ascii_char1, ascii_char2, ascii_char3, ascii_char4, ascii_char5, ascii_char6 end --Декодирование UTF-8 символа local function decodeChar(file) local first_byte = readBytes(file, 1) local charcode_array = {first_byte} local len = 1 local middle = selectTerminateBit(first_byte) if ( middle == 32 ) then len = 2 elseif ( middle == 16 ) then len = 3 elseif ( middle == 8 ) then len = 4 elseif ( middle == 4 ) then len = 5 elseif ( middle == 2 ) then len = 6 end for i = 1, len-1 do table.insert( charcode_array, readBytes(file, 1) ) end return string.char( table.unpack( charcode_array ) ) end --Правильное конвертирование HEX-переменной в строковую local function HEXtoSTRING(color, bitCount, withNull) local stro4ka = string.format("%X",color) local sStro4ka = unicode.len(stro4ka) if sStro4ka < bitCount then stro4ka = string.rep("0", bitCount - sStro4ka) .. stro4ka end sStro4ka = nil if withNull then return "0x"..stro4ka else return stro4ka end end --Получение формата файла local function getFileFormat(path) local name = fs.name(path) local starting, ending = string.find(name, "(.)%.[%d%w]*$") if starting == nil then return nil else return unicode.sub(name,starting + 1, -1) end name, starting, ending = nil, nil, nil end ------------------------------ Все, что касается сжатого формата ------------------------------------------------------------ -- Запись в файл сжатого OCIF-формата изображения function image.saveCompressed(path, picture) local encodedPixel local file = assert( io.open(path, "w"), constants.fileOpenError ) file:write( table.unpack( ocif_signature_expand ) ) file:write( string.char( picture.width ) ) file:write( string.char( picture.height ) ) for i = 1, picture.width * picture.height * constants.elementCount, constants.elementCount do encodedPixel = { encodePixel(picture[i], picture[i + 1], picture[i + 2], picture[i + 3]) } for j = 1, #encodedPixel do file:write( string.char( encodedPixel[j] ) ) end end file:close() end --Чтение из файла сжатого OCIF-формата изображения, возвращает массив типа 2 (подробнее о типах см. конец файла) function image.loadCompressed(path) local picture = {} local file = assert( io.open(path, "rb"), constants.fileOpenError ) --Проверка файла на соответствие сигнатуры local signature1, signature2 = readBytes(file, 4), readBytes(file, 3) if ( signature1 ~= ocif_signature1 or signature2 ~= ocif_signature2 ) then file:close() return nil end --Читаем ширину и высоту файла picture.width = readBytes(file, 1) picture.height = readBytes(file, 1) for i = 1, picture.width * picture.height do --Читаем бекграунд table.insert(picture, readBytes(file, 3)) --Читаем форграунд table.insert(picture, readBytes(file, 3)) --Читаем альфу table.insert(picture, readBytes(file, 1)) --Читаем символ table.insert(picture, decodeChar( file )) end file:close() return picture end ------------------------------ Все, что касается сырого формата ------------------------------------------------------------ --Сохранение в файл сырого формата изображения типа 2 (подробнее о типах см. конец файла) function image.saveRaw(path, picture) local file = assert( io.open(path, "w"), constants.fileOpenError ) local xPos, yPos = 1, 1 for i = 1, picture.width * picture.height * constants.elementCount, constants.elementCount do file:write( HEXtoSTRING(picture[i], 6), " ", HEXtoSTRING(picture[i + 1], 6), " ", HEXtoSTRING(picture[i + 2], 2), " ", picture[i + 3], " ") xPos = xPos + 1 if xPos > picture.width then xPos = 1 yPos = yPos + 1 file:write("\n") end end file:close() end --Загрузка из файла сырого формата изображения типа 2 (подробнее о типах см. конец файла) function image.loadRaw(path) local file = assert( io.open(path, "r"), constants.fileOpenError ) local picture = {} local background, foreground, alpha, symbol, sLine local lineCounter = 0 for line in file:lines() do sLine = unicode.len(line) for i = 1, sLine, constants.rawImageLoadStep do background = "0x" .. unicode.sub(line, i, i + 5) foreground = "0x" .. unicode.sub(line, i + 7, i + 12) alpha = "0x" .. unicode.sub(line, i + 14, i + 15) symbol = unicode.sub(line, i + 17, i + 17) table.insert(picture, tonumber(background)) table.insert(picture, tonumber(foreground)) table.insert(picture, tonumber(alpha)) table.insert(picture, symbol) end lineCounter = lineCounter + 1 end picture.width = sLine / constants.rawImageLoadStep picture.height = lineCounter file:close() return picture end ----------------------------------- Все, что касается реального PNG-формата ------------------------------------------------------------ function image.loadPng(path) if not _G.libPNGImage then _G.libPNGImage = require("libPNGImage") end local success, pngImageOrErrorMessage = pcall(libPNGImage.newFromFile, path) if not success then io.stderr:write(" * PNGView: PNG Loading Error *\n") io.stderr:write("While attempting to load '" .. path .. "' as PNG, libPNGImage erred:\n") io.stderr:write(pngImageOrErrorMessage) return end local picture = {} picture.width, picture.height = pngImageOrErrorMessage:getSize() local r, g, b, a, hex for j = 0, picture.height - 1 do for i = 0, picture.width - 1 do r, g, b, a = pngImageOrErrorMessage:getPixel(i, j) if r and g and b and a and a > 0 then hex = colorlib.RGBtoHEX(r, g, b) table.insert(picture, hex) table.insert(picture, 0x000000) table.insert(picture, 0x00) table.insert(picture, " ") end end end return picture end ----------------------------------- Вспомогательные функции программы ------------------------------------------------------------ --Оптимизировать и сгруппировать по цветам картинку типа 2 (подробнее о типах см. конец файла) function image.convertToGroupedImage(picture) --Создаем массив оптимизированной картинки local optimizedPicture = {} --Задаем константы local xPos, yPos, background, foreground, alpha, symbol = 1, 1, nil, nil, nil, nil --Перебираем все элементы массива for i = 1, picture.width * picture.height * constants.elementCount, constants.elementCount do --Получаем символ из неоптимизированного массива background, foreground, alpha, symbol = picture[i], picture[i + 1], picture[i + 2], picture[i + 3] --Группируем картинку по цветам optimizedPicture[background] = optimizedPicture[background] or {} optimizedPicture[background][foreground] = optimizedPicture[background][foreground] or {} table.insert(optimizedPicture[background][foreground], xPos) table.insert(optimizedPicture[background][foreground], yPos) table.insert(optimizedPicture[background][foreground], alpha) table.insert(optimizedPicture[background][foreground], symbol) --Если xPos достигает width изображения, то сбросить на 1, иначе xPos++ xPos = (xPos == picture.width) and 1 or xPos + 1 --Если xPos равняется 1, то yPos++, а если нет, то похуй yPos = (xPos == 1) and yPos + 1 or yPos end --Возвращаем оптимизированный массив return optimizedPicture end --Нарисовать по указанным координатам картинку указанной ширины и высоты для теста function image.drawRandomImage(x, y, width, height) local picture = {} local symbolArray = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "А", "Б", "В", "Г", "Д", "Е", "Ж", "З", "И", "Й", "К", "Л", "И", "Н", "О", "П", "Р", "С", "Т", "У", "Ф", "Х", "Ц", "Ч", "Ш", "Щ", "Ъ", "Ы", "Ь", "Э", "Ю", "Я"} picture.width = width picture.height = height local background, foreground, symbol for j = 1, height do for i = 1, width do background = math.random(0x000000, 0xffffff) foreground = math.random(0x000000, 0xffffff) symbol = symbolArray[math.random(1, #symbolArray)] table.insert(picture, background) table.insert(picture, foreground) table.insert(picture, 0x00) table.insert(picture, symbol) end end image.draw(x, y, picture) return picture end ----------------------------------------- Основные функции программы ------------------------------------------------------------------- --Сохранить изображение любого поддерживаемого формата function image.save(path, picture) --Создать папку под файл, если ее нет fs.makeDirectory(fs.path(path)) --Получаем формат указанного файла local fileFormat = getFileFormat(path) --Проверяем соответствие формата файла if fileFormat == constants.compressedFileFormat then image.saveCompressed(path, picture) elseif fileFormat == constants.rawFileFormat then image.saveRaw(path, picture) else error("Unsupported file format.\n") end end --Загрузить изображение любого поддерживаемого формата function image.load(path) --Кинуть ошибку, если такого файла не существует if not fs.exists(path) then error("File \""..path.."\" does not exists.\n") end --Получаем формат указанного файла local fileFormat = getFileFormat(path) --Проверяем соответствие формата файла if fileFormat == constants.compressedFileFormat then return image.loadCompressed(path) elseif fileFormat == constants.rawFileFormat then return image.loadRaw(path) elseif fileFormat == constants.pngFileFormat then return image.loadPng(path) else error("Unsupported file format.\n") end end --Отрисовка изображения типа 3 (подробнее о типах см. конец файла) function image.draw(x, y, rawPicture) --Конвертируем в групповое изображение local picture = image.convertToGroupedImage(rawPicture) --Все как обычно x, y = x - 1, y - 1 --Переменные, чтобы в цикле эту парашу не создавать local currentBackground, xPos, yPos, alpha, symbol local _, _ --Перебираем все цвета фона for background in pairs(picture) do --Заранее ставим корректный цвет фона gpu.setBackground(background) --Перебираем все цвета текста for foreground in pairs(picture[background]) do --Ставим сразу и корректный цвет текста gpu.setForeground(foreground) --Перебираем все пиксели for i = 1, #picture[background][foreground], 4 do --Получаем временную репрезентацию xPos, yPos, alpha, symbol = picture[background][foreground][i], picture[background][foreground][i + 1], picture[background][foreground][i + 2], picture[background][foreground][i + 3] --Если альфа имеется, но она не совсем прозрачна if (alpha > 0x00 and alpha < 0xFF) or (alpha == 0xFF and symbol ~= " ")then _, _, currentBackground = gpu.get(x + xPos, y + yPos) currentBackground = colorlib.alphaBlend(currentBackground, background, alpha) gpu.setBackground(currentBackground) --Рисуем символ на экране gpu.set(x + xPos, y + yPos, symbol) --Если альфа отсутствует elseif alpha == 0x00 then if currentBackground ~= background then currentBackground = background gpu.setBackground(currentBackground) end --Рисуем символ на экране gpu.set(x + xPos, y + yPos, symbol) end --Выгружаем сгруппированное изображение из памяти picture[background][foreground][i], picture[background][foreground][i + 1], picture[background][foreground][i + 2], picture[background][foreground][i + 3] = nil, nil, nil, nil end --Выгружаем данные о текущем цвете текста из памяти picture[background][foreground] = nil end --Выгружаем данные о текущем фоне из памяти picture[background] = nil end end local function loadOldPng(path) local massiv = {} local file = io.open(path, "r") local lineCounter = 0 local sLine = 0 for line in file:lines() do sLine = unicode.len(line) for i = 1, sLine, 16 do table.insert(massiv, tonumber("0x" .. unicode.sub(line, i, i + 5))) table.insert(massiv, tonumber("0x" .. unicode.sub(line, i + 7, i + 12))) table.insert(massiv, 0x00) table.insert(massiv, tonumber("0x" .. unicode.sub(line, i + 14, i + 14))) end lineCounter = lineCounter + 1 end massiv.width = sLine / 16 massiv.height = lineCounter return massiv end --Сделать скриншот экрана и сохранить его по указанному пути function image.screenshot(path) local picture = {} local foreground, background, symbol picture.width, picture.height = gpu.getResolution() for j = 1, picture.height do for i = 1, picture.width do symbol, foreground, background = gpu.get(i, j) table.insert(picture, background) table.insert(picture, foreground) table.insert(picture, 0x00) table.insert(picture, symbol) end end image.save(path, picture) end ------------------------------------------ Примеры работы с библиотекой ------------------------------------------------ -- ecs.prepareToExit() -- ecs.error("Создаю и рисую картинку!") -- local generatedPic = image.drawRandomImage(1, 1, 160, 50) -- ecs.error("Сохраняю созданную картинку в формате .pic!") -- ecs.prepareToExit() -- image.save("1.pic", generatedPic) -- ecs.error("Сохраняю созданную картинку в формате .rawpic!") -- ecs.prepareToExit() -- image.save("1.rawpic", generatedPic) -- ecs.error("Загружаю сохраненную картинку в формате .pic!") -- ecs.prepareToExit() -- local loadedPic = image.load("1.pic") -- image.draw(1, 1, loadedPic) -- ecs.error("Загружаю сохраненную картинку в формате .rawpic!") -- ecs.prepareToExit() -- local loadedPic = image.load("1.rawpic") -- image.draw(1, 1, loadedPic) ------------------------------------------ Типы массивов изображений --------------------------------------------------- --[[ Тип 1: Основной формат изображения, линейная запись данных о пикселях, сжатие двух цветов и альфа-канала в одно число. Минимальный расход оперативной памяти, однако для изменения цвета требует декомпрессию. Структура: local picture = { width = ширина изображения, height = высота изображения, Сжатые цвета и альфа-канал, Символ, Сжатые цвета и альфа-канал, Символ, ... } Пример: local picture = { width = 2, height = 2, 0xff00aa, "Q", 0x88aacc, "W", 0xff00aa, "E", 0x88aacc, "R" } Тип 2 конвертируется только в тип 3 и только для отрисовки на экране: Изображение типа 3 = image.convertToGroupedImage( Сюда кидаем массив изображения типа 2 ) Тип 2 (сгруппированный по цветам формат, ипользуется только для отрисовки изображения): Структура: local picture = { Цвет фона 1 = { Цвет текста 1 = { Координата по X, Координата по Y, Альфа-канал, Символ, Координата по X, Координата по Y, Альфа-канал, Символ, ... }, Цвет текста 2 = { Координата по X, Координата по Y, Альфа-канал, Символ, Координата по X, Координата по Y, Альфа-канал, Символ, ... }, ... }, Цвет фона 2 = { Цвет текста 1 = { Координата по X, Координата по Y, Альфа-канал, Символ, Координата по X, Координата по Y, Альфа-канал, Символ, ... }, Цвет текста 2 = { Координата по X, Координата по Y, Альфа-канал, Символ, Координата по X, Координата по Y, Альфа-канал, Символ, ... }, ... }, } Пример: local picture = { 0xffffff = { 0xaabbcc = { 1, 1, 0x00, "Q", 12, 12, 0xaa, "W" }, 0x88aa44 = { 5, 5, 0xcc, "E", 12, 12, 0x00, "R" } }, 0x444444 = { 0x112233 = { 40, 20, 0x00, "T", 12, 12, 0xaa, "Y" }, 0x88aa44 = { 5, 5, 0xcc, "U", 12, 12, 0x00, "I" } } } ]] ------------------------------------------------------------------------------------------------------------------------ return image