MineOS/lib/image.lua

634 lines
22 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-------------------------------------------- OCIF Image Format -----------------------------------------------------------
local copyright = [[
Автор: Pirnogion
VK: https://vk.com/id88323331
Соавтор: IT
VK: https://vk.com/id7799889
]]
--------------------------------------- Подгрузка библиотек --------------------------------------------------------------
local component = require("component")
local unicode = require("unicode")
local fs = require("filesystem")
local gpu = component.gpu
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",
}
---------------------------------------- Локальные функции -------------------------------------------------------------------
--Выделить бит-терминатор в первом байте 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
--Преобразует цвет из HEX-записи в RGB-запись
local function HEXtoRGB(color)
return bit32.rshift( color, 16 ), bit32.rshift( bit32.band(color, 0x00ff00), 8 ), bit32.band(color, 0x0000ff)
end
--Аналогично, но из RGB в HEX
local function RGBtoHEX(rr, gg, bb)
return bit32.lshift(rr, 16) + bit32.lshift(gg, 8) + bb
end
--Смешивание двух цветов на основе альфа-канала второго
local function alphaBlend(back_color, front_color, alpha_channel)
local INVERTED_ALPHA_CHANNEL = 255 - alpha_channel
local back_color_rr, back_color_gg, back_color_bb = HEXtoRGB(back_color)
local front_color_rr, front_color_gg, front_color_bb = HEXtoRGB(front_color)
local blended_rr = front_color_rr * INVERTED_ALPHA_CHANNEL / 255 + back_color_rr * alpha_channel / 255
local blended_gg = front_color_gg * INVERTED_ALPHA_CHANNEL / 255 + back_color_gg * alpha_channel / 255
local blended_bb = front_color_bb * INVERTED_ALPHA_CHANNEL / 255 + back_color_bb * alpha_channel / 255
return RGBtoHEX( blended_rr, blended_gg, blended_bb )
end
--Подготавливает цвета и символ для записи в файл сжатого формата
local function encodePixel(background, foreground, alpha, char)
--Расхерачиваем жирные цвета в компактные цвета
local ascii_background1, ascii_background2, ascii_background3 = HEXtoRGB(background)
local ascii_foreground1, ascii_foreground2, ascii_foreground3 = 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
----------------------------------- Вспомогательные функции программы ------------------------------------------------------------
--Оптимизировать и сгруппировать по цветам картинку типа 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)
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 = 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