Created
May 10, 2026 05:38
-
-
Save HaydenReeve/d1f75e4bb4aa9d38c1fc86c592976ae2 to your computer and use it in GitHub Desktop.
Import Paint.NET .pdn files into Aseprite while preserving supported bitmap layers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- Import PDN to Aseprite | |
| -- Installation: | |
| -- 1. Copy the whole "Import PDN to Aseprite" folder into your Aseprite scripts folder. | |
| -- 2. In Aseprite, run File > Scripts > Rescan Scripts, or restart Aseprite. | |
| -- 3. Run File > Scripts > Import PDN to Aseprite. | |
| -- | |
| -- This script is pure Lua. It reads Paint.NET .pdn files directly and rebuilds | |
| -- supported bitmap layers inside a new Aseprite sprite. | |
| -- | |
| -- Embedded parser/decompression logic is adapted from two MIT-licensed projects: | |
| -- - pypdn by Addison Elliott | |
| -- - lua-compress-deflatelua by David Manura | |
| local COMMAND_TITLE = "Import PDN to Aseprite" | |
| local function fail(message) | |
| if app.isUIAvailable then | |
| app.alert { | |
| title = COMMAND_TITLE, | |
| text = tostring(message) | |
| } | |
| return | |
| end | |
| error(tostring(message), 0) | |
| end | |
| local function hasValue(value) | |
| return value ~= nil and value ~= "" | |
| end | |
| local function trim(value) | |
| return tostring(value):gsub("^%s+", ""):gsub("%s+$", "") | |
| end | |
| local LUA_KEYWORDS = { | |
| ["and"] = true, ["break"] = true, ["do"] = true, ["else"] = true, | |
| ["elseif"] = true, ["end"] = true, ["false"] = true, ["for"] = true, | |
| ["function"] = true, ["goto"] = true, ["if"] = true, ["in"] = true, | |
| ["local"] = true, ["nil"] = true, ["not"] = true, ["or"] = true, | |
| ["repeat"] = true, ["return"] = true, ["then"] = true, ["true"] = true, | |
| ["until"] = true, ["while"] = true | |
| } | |
| local function sanitizeIdentifier(identifier) | |
| identifier = tostring(identifier):gsub("[^%w_]", "_"):gsub("^[%d_]+", "") | |
| if identifier == "" then | |
| identifier = "_" | |
| end | |
| if LUA_KEYWORDS[identifier] then | |
| identifier = identifier .. "_" | |
| end | |
| return identifier | |
| end | |
| local function makeByteCollector() | |
| local chunks = {} | |
| local buffer = {} | |
| local bufferSize = 0 | |
| local flushLimit = 2048 | |
| local function flush() | |
| if bufferSize == 0 then | |
| return | |
| end | |
| chunks[#chunks + 1] = string.char(table.unpack(buffer, 1, bufferSize)) | |
| buffer = {} | |
| bufferSize = 0 | |
| end | |
| local function writeByte(byte) | |
| bufferSize = bufferSize + 1 | |
| buffer[bufferSize] = byte | |
| if bufferSize >= flushLimit then | |
| flush() | |
| end | |
| end | |
| local function finish() | |
| flush() | |
| return table.concat(chunks) | |
| end | |
| return writeByte, finish | |
| end | |
| local function buildGunzip() | |
| local function runtimeError(message) | |
| error(message, 0) | |
| end | |
| local function newBitStream(input) | |
| return { | |
| data = input, | |
| byteIndex = 1, | |
| bitBuffer = 0, | |
| bitCount = 0 | |
| } | |
| end | |
| local function readBits(stream, bitCount) | |
| bitCount = bitCount or 1 | |
| while stream.bitCount < bitCount do | |
| local byte = stream.data:byte(stream.byteIndex) | |
| if byte == nil then | |
| return nil | |
| end | |
| stream.bitBuffer = stream.bitBuffer | (byte << stream.bitCount) | |
| stream.bitCount = stream.bitCount + 8 | |
| stream.byteIndex = stream.byteIndex + 1 | |
| end | |
| if bitCount == 0 then | |
| return 0 | |
| end | |
| local mask = bitCount == 32 and 0xFFFFFFFF or ((1 << bitCount) - 1) | |
| local bits = stream.bitBuffer & mask | |
| stream.bitBuffer = stream.bitBuffer >> bitCount | |
| stream.bitCount = stream.bitCount - bitCount | |
| return bits | |
| end | |
| local function alignToByte(stream) | |
| stream.bitBuffer = 0 | |
| stream.bitCount = 0 | |
| end | |
| local function hasBit(bits, bit) | |
| return bits % (bit + bit) >= bit | |
| end | |
| local function reverseBits(value, bitCount) | |
| local reversed = 0 | |
| for _ = 1, bitCount do | |
| reversed = (reversed << 1) | (value & 1) | |
| value = value >> 1 | |
| end | |
| return reversed | |
| end | |
| local function buildHuffmanTable(init, isFull) | |
| local entries = {} | |
| if isFull then | |
| for value, bitCount in pairs(init) do | |
| if bitCount ~= 0 then | |
| entries[#entries + 1] = { value = value, bitCount = bitCount } | |
| end | |
| end | |
| else | |
| local index = 1 | |
| while index <= #init - 2 do | |
| local firstValue = init[index] | |
| local bitCount = init[index + 1] | |
| local nextValue = init[index + 2] | |
| if bitCount ~= 0 then | |
| for value = firstValue, nextValue - 1 do | |
| entries[#entries + 1] = { value = value, bitCount = bitCount } | |
| end | |
| end | |
| index = index + 2 | |
| end | |
| end | |
| table.sort(entries, function(a, b) | |
| if a.bitCount == b.bitCount then | |
| return a.value < b.value | |
| end | |
| return a.bitCount < b.bitCount | |
| end) | |
| local code = 1 | |
| local previousBits = 0 | |
| local lookup = {} | |
| local minimumBits = math.huge | |
| for _, entry in ipairs(entries) do | |
| if entry.bitCount ~= previousBits then | |
| code = code << (entry.bitCount - previousBits) | |
| previousBits = entry.bitCount | |
| end | |
| entry.code = code | |
| lookup[code] = entry.value | |
| code = code + 1 | |
| if entry.bitCount < minimumBits then | |
| minimumBits = entry.bitCount | |
| end | |
| end | |
| return { | |
| minBits = minimumBits, | |
| lookup = lookup | |
| } | |
| end | |
| local function readHuffmanValue(stream, tableInfo) | |
| local code = 1 | |
| local bitCount = 0 | |
| while true do | |
| if bitCount == 0 then | |
| local bits = readBits(stream, tableInfo.minBits) | |
| if bits == nil then | |
| runtimeError("Unexpected end of compressed stream.") | |
| end | |
| code = (1 << tableInfo.minBits) | reverseBits(bits, tableInfo.minBits) | |
| bitCount = tableInfo.minBits | |
| else | |
| local bit = readBits(stream, 1) | |
| if bit == nil then | |
| runtimeError("Unexpected end of compressed stream.") | |
| end | |
| bitCount = bitCount + 1 | |
| code = (code << 1) | bit | |
| end | |
| local value = tableInfo.lookup[code] | |
| if value ~= nil then | |
| return value | |
| end | |
| end | |
| end | |
| local function parseGzipHeader(stream) | |
| local id1 = readBits(stream, 8) | |
| local id2 = readBits(stream, 8) | |
| if id1 ~= 31 or id2 ~= 139 then | |
| runtimeError("Compressed chunk is not in gzip format.") | |
| end | |
| local compressionMethod = readBits(stream, 8) | |
| local flags = readBits(stream, 8) | |
| if compressionMethod ~= 8 then | |
| runtimeError("Unsupported gzip compression method.") | |
| end | |
| readBits(stream, 32) -- mtime | |
| readBits(stream, 8) -- xfl | |
| readBits(stream, 8) -- os | |
| if hasBit(flags, 2 ^ 2) then | |
| local extraLength = readBits(stream, 16) | |
| for _ = 1, extraLength do | |
| readBits(stream, 8) | |
| end | |
| end | |
| local function readZeroTerminatedString() | |
| while true do | |
| local byte = readBits(stream, 8) | |
| if byte == nil then | |
| runtimeError("Invalid gzip header.") | |
| end | |
| if byte == 0 then | |
| return | |
| end | |
| end | |
| end | |
| if hasBit(flags, 2 ^ 3) then | |
| readZeroTerminatedString() | |
| end | |
| if hasBit(flags, 2 ^ 4) then | |
| readZeroTerminatedString() | |
| end | |
| if hasBit(flags, 2 ^ 1) then | |
| readBits(stream, 16) | |
| end | |
| end | |
| local function parseDynamicTables(stream) | |
| local literalCount = readBits(stream, 5) + 257 | |
| local distanceCount = readBits(stream, 5) + 1 | |
| local codeLengthCount = readBits(stream, 4) + 4 | |
| local codeLengthOrder = { | |
| 16, 17, 18, 0, 8, 7, 9, 6, 10, | |
| 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 | |
| } | |
| local codeLengthInit = {} | |
| for index = 1, codeLengthCount do | |
| codeLengthInit[codeLengthOrder[index]] = readBits(stream, 3) | |
| end | |
| local codeLengthTable = buildHuffmanTable(codeLengthInit, true) | |
| local function decode(count) | |
| local init = {} | |
| local value = 0 | |
| local previousBits = 0 | |
| while value < count do | |
| local codeLength = readHuffmanValue(stream, codeLengthTable) | |
| local repeatCount | |
| local bitCount | |
| if codeLength <= 15 then | |
| repeatCount = 1 | |
| bitCount = codeLength | |
| previousBits = bitCount | |
| elseif codeLength == 16 then | |
| repeatCount = 3 + readBits(stream, 2) | |
| bitCount = previousBits | |
| elseif codeLength == 17 then | |
| repeatCount = 3 + readBits(stream, 3) | |
| bitCount = 0 | |
| previousBits = 0 | |
| elseif codeLength == 18 then | |
| repeatCount = 11 + readBits(stream, 7) | |
| bitCount = 0 | |
| previousBits = 0 | |
| else | |
| runtimeError("Invalid dynamic Huffman code length.") | |
| end | |
| for _ = 1, repeatCount do | |
| init[value] = bitCount | |
| value = value + 1 | |
| end | |
| end | |
| return buildHuffmanTable(init, true) | |
| end | |
| return decode(literalCount), decode(distanceCount) | |
| end | |
| local LENGTH_BASE = { | |
| [257] = 3, [258] = 4, [259] = 5, [260] = 6, [261] = 7, [262] = 8, [263] = 9, [264] = 10, | |
| [265] = 11, [266] = 13, [267] = 15, [268] = 17, [269] = 19, [270] = 23, [271] = 27, [272] = 31, | |
| [273] = 35, [274] = 43, [275] = 51, [276] = 59, [277] = 67, [278] = 83, [279] = 99, [280] = 115, | |
| [281] = 131, [282] = 163, [283] = 195, [284] = 227, [285] = 258 | |
| } | |
| local LENGTH_EXTRA = { | |
| [257] = 0, [258] = 0, [259] = 0, [260] = 0, [261] = 0, [262] = 0, [263] = 0, [264] = 0, | |
| [265] = 1, [266] = 1, [267] = 1, [268] = 1, [269] = 2, [270] = 2, [271] = 2, [272] = 2, | |
| [273] = 3, [274] = 3, [275] = 3, [276] = 3, [277] = 4, [278] = 4, [279] = 4, [280] = 4, | |
| [281] = 5, [282] = 5, [283] = 5, [284] = 5, [285] = 0 | |
| } | |
| local DIST_BASE = { | |
| [0] = 1, [1] = 2, [2] = 3, [3] = 4, [4] = 5, [5] = 7, [6] = 9, [7] = 13, | |
| [8] = 17, [9] = 25, [10] = 33, [11] = 49, [12] = 65, [13] = 97, [14] = 129, [15] = 193, | |
| [16] = 257, [17] = 385, [18] = 513, [19] = 769, [20] = 1025, [21] = 1537, [22] = 2049, [23] = 3073, | |
| [24] = 4097, [25] = 6145, [26] = 8193, [27] = 12289, [28] = 16385, [29] = 24577 | |
| } | |
| local DIST_EXTRA = { | |
| [0] = 0, [1] = 0, [2] = 0, [3] = 0, [4] = 1, [5] = 1, [6] = 2, [7] = 2, | |
| [8] = 3, [9] = 3, [10] = 4, [11] = 4, [12] = 5, [13] = 5, [14] = 6, [15] = 6, | |
| [16] = 7, [17] = 7, [18] = 8, [19] = 8, [20] = 9, [21] = 9, [22] = 10, [23] = 10, | |
| [24] = 11, [25] = 11, [26] = 12, [27] = 12, [28] = 13, [29] = 13 | |
| } | |
| local function newOutputState() | |
| local writeByte, finish = makeByteCollector() | |
| return { | |
| window = {}, | |
| windowPosition = 1, | |
| writeByte = writeByte, | |
| finish = finish | |
| } | |
| end | |
| local function emitByte(state, byte) | |
| state.writeByte(byte) | |
| state.window[state.windowPosition] = byte | |
| state.windowPosition = (state.windowPosition % 32768) + 1 | |
| end | |
| local function parseCompressedItem(stream, state, literalTable, distanceTable) | |
| local value = readHuffmanValue(stream, literalTable) | |
| if value < 256 then | |
| emitByte(state, value) | |
| return false | |
| end | |
| if value == 256 then | |
| return true | |
| end | |
| local lengthBase = LENGTH_BASE[value] | |
| local lengthExtraBits = LENGTH_EXTRA[value] | |
| local length = lengthBase + readBits(stream, lengthExtraBits) | |
| local distanceValue = readHuffmanValue(stream, distanceTable) | |
| local distanceBase = DIST_BASE[distanceValue] | |
| local distanceExtraBits = DIST_EXTRA[distanceValue] | |
| local distance = distanceBase + readBits(stream, distanceExtraBits) | |
| for _ = 1, length do | |
| local position = ((state.windowPosition - 1 - distance) % 32768) + 1 | |
| local byte = state.window[position] | |
| if byte == nil then | |
| runtimeError("Invalid deflate distance.") | |
| end | |
| emitByte(state, byte) | |
| end | |
| return false | |
| end | |
| local FIXED_LITERAL_TABLE = buildHuffmanTable { 0, 8, 144, 9, 256, 7, 280, 8, 288, nil } | |
| local FIXED_DISTANCE_TABLE = buildHuffmanTable { 0, 5, 32, nil } | |
| local function parseBlock(stream, state) | |
| local isFinal = readBits(stream, 1) | |
| local blockType = readBits(stream, 2) | |
| if blockType == 0 then | |
| alignToByte(stream) | |
| local length = readBits(stream, 16) | |
| readBits(stream, 16) -- nlen | |
| for _ = 1, length do | |
| local byte = readBits(stream, 8) | |
| if byte == nil then | |
| runtimeError("Unexpected end of stored deflate block.") | |
| end | |
| emitByte(state, byte) | |
| end | |
| elseif blockType == 1 or blockType == 2 then | |
| local literalTable = FIXED_LITERAL_TABLE | |
| local distanceTable = FIXED_DISTANCE_TABLE | |
| if blockType == 2 then | |
| literalTable, distanceTable = parseDynamicTables(stream) | |
| end | |
| while true do | |
| if parseCompressedItem(stream, state, literalTable, distanceTable) then | |
| break | |
| end | |
| end | |
| else | |
| runtimeError("Unsupported deflate block type.") | |
| end | |
| return isFinal ~= 0 | |
| end | |
| local function gunzipString(input) | |
| local stream = newBitStream(input) | |
| parseGzipHeader(stream) | |
| local state = newOutputState() | |
| repeat | |
| local isFinal = parseBlock(stream, state) | |
| if isFinal then | |
| break | |
| end | |
| until false | |
| alignToByte(stream) | |
| readBits(stream, 32) -- crc32 | |
| readBits(stream, 32) -- isize | |
| return state.finish() | |
| end | |
| return gunzipString | |
| end | |
| local gunzipString = buildGunzip() | |
| local function createBinaryReader(path) | |
| local handle, openError = io.open(path, "rb") | |
| if not handle then | |
| error("Failed to open file: " .. tostring(openError), 0) | |
| end | |
| local reader = { | |
| handle = handle, | |
| position = 0 | |
| } | |
| function reader:close() | |
| if self.handle then | |
| self.handle:close() | |
| self.handle = nil | |
| end | |
| end | |
| function reader:readExact(count) | |
| local data = self.handle:read(count) | |
| if type(data) ~= "string" or #data ~= count then | |
| error(string.format("Unexpected end of file at byte %d.", self.position), 0) | |
| end | |
| self.position = self.position + count | |
| return data | |
| end | |
| function reader:readByte() | |
| return self:readExact(1):byte(1) | |
| end | |
| function reader:readUInt24LE() | |
| local a, b, c = self:readExact(3):byte(1, 3) | |
| return a + b * 256 + c * 65536 | |
| end | |
| function reader:readUInt16LE() | |
| local a, b = self:readExact(2):byte(1, 2) | |
| return a + b * 256 | |
| end | |
| function reader:readInt16LE() | |
| local value = self:readUInt16LE() | |
| if value >= 32768 then | |
| return value - 65536 | |
| end | |
| return value | |
| end | |
| function reader:readUInt32LE() | |
| local a, b, c, d = self:readExact(4):byte(1, 4) | |
| return a + b * 256 + c * 65536 + d * 16777216 | |
| end | |
| function reader:readInt32LE() | |
| local value = self:readUInt32LE() | |
| if value >= 2147483648 then | |
| return value - 4294967296 | |
| end | |
| return value | |
| end | |
| function reader:readUInt32BE() | |
| local a, b, c, d = self:readExact(4):byte(1, 4) | |
| return ((a * 256 + b) * 256 + c) * 256 + d | |
| end | |
| function reader:readInt64LE() | |
| local lo = self:readUInt32LE() | |
| local hi = self:readUInt32LE() | |
| local value = lo + hi * 4294967296 | |
| if hi >= 2147483648 then | |
| value = value - 18446744073709551616 | |
| end | |
| return value | |
| end | |
| function reader:readUInt64LE() | |
| local lo = self:readUInt32LE() | |
| local hi = self:readUInt32LE() | |
| return lo + hi * 4294967296 | |
| end | |
| function reader:readLengthPrefixedString() | |
| local length = 0 | |
| local shift = 0 | |
| for _ = 1, 5 do | |
| local byte = self:readByte() | |
| length = length + ((byte & 0x7F) << shift) | |
| if (byte & 0x80) == 0 then | |
| return self:readExact(length) | |
| end | |
| shift = shift + 7 | |
| end | |
| error("NRBF length-prefixed string overflow.", 0) | |
| end | |
| return reader | |
| end | |
| local PrimitiveType = { | |
| Boolean = 1, | |
| Byte = 2, | |
| Char = 3, | |
| Decimal = 5, | |
| Double = 6, | |
| Int16 = 7, | |
| Int32 = 8, | |
| Int64 = 9, | |
| SByte = 10, | |
| Single = 11, | |
| TimeSpan = 12, | |
| DateTime = 13, | |
| UInt16 = 14, | |
| UInt32 = 15, | |
| UInt64 = 16, | |
| Null = 17, | |
| String = 18 | |
| } | |
| local BinaryType = { | |
| Primitive = 0, | |
| String = 1, | |
| Object = 2, | |
| SystemClass = 3, | |
| Class = 4, | |
| ObjectArray = 5, | |
| StringArray = 6, | |
| PrimitiveArray = 7 | |
| } | |
| local BinaryArrayType = { | |
| Single = 0, | |
| Jagged = 1, | |
| Rectangular = 2, | |
| SingleOffset = 3, | |
| JaggedOffset = 4, | |
| RectangularOffset = 5 | |
| } | |
| local RecordType = { | |
| SerializedStreamHeader = 0, | |
| ClassWithId = 1, | |
| SystemClassWithMembers = 2, | |
| ClassWithMembers = 3, | |
| SystemClassWithMembersAndTypes = 4, | |
| ClassWithMembersAndTypes = 5, | |
| BinaryObjectString = 6, | |
| BinaryArray = 7, | |
| MemberPrimitiveTyped = 8, | |
| MemberReference = 9, | |
| ObjectNull = 10, | |
| MessageEnd = 11, | |
| BinaryLibrary = 12, | |
| ObjectNullMultiple256 = 13, | |
| ObjectNullMultiple = 14, | |
| ArraySinglePrimitive = 15, | |
| ArraySingleObject = 16, | |
| ArraySingleString = 17 | |
| } | |
| local function newReference(objectId) | |
| return { | |
| __kind = "reference", | |
| objectId = objectId, | |
| parent = nil, | |
| index = nil | |
| } | |
| end | |
| local function isReference(value) | |
| return type(value) == "table" and value.__kind == "reference" | |
| end | |
| local function newObjectNullMultiple(count) | |
| return { | |
| __kind = "object-null-multiple", | |
| count = count | |
| } | |
| end | |
| local function isObjectNullMultiple(value) | |
| return type(value) == "table" and value.__kind == "object-null-multiple" | |
| end | |
| local function newBinaryLibrary(libraryId, name) | |
| return { | |
| __kind = "binary-library", | |
| id = libraryId, | |
| name = name, | |
| objects = {} | |
| } | |
| end | |
| local function isBinaryLibrary(value) | |
| return type(value) == "table" and value.__kind == "binary-library" | |
| end | |
| local function createClassInfo(objectId, className, fieldNames, isSystemClass) | |
| return { | |
| id = objectId, | |
| originalClassName = className, | |
| className = sanitizeIdentifier(className), | |
| fieldNames = fieldNames, | |
| typeInfo = nil, | |
| isSystemClass = isSystemClass | |
| } | |
| end | |
| local function createObject(classInfo, objectId) | |
| local obj = { | |
| __classInfo = classInfo, | |
| __className = classInfo.className, | |
| __originalClassName = classInfo.originalClassName, | |
| __fieldNames = classInfo.fieldNames, | |
| __id = objectId | |
| } | |
| for index, fieldName in ipairs(classInfo.fieldNames) do | |
| obj[index] = nil | |
| obj[fieldName] = nil | |
| end | |
| return obj | |
| end | |
| local function setIndexedValue(container, index, value) | |
| container[index] = value | |
| if container.__fieldNames then | |
| local fieldName = container.__fieldNames[index] | |
| if fieldName then | |
| container[fieldName] = value | |
| end | |
| end | |
| end | |
| local function getContainerLength(container) | |
| if type(container) ~= "table" then | |
| return 0 | |
| end | |
| if container.__fieldNames then | |
| return #container.__fieldNames | |
| end | |
| return container.__length or #container | |
| end | |
| local function convert1DArrayND(array1d, dims, state) | |
| state = state or { index = 1 } | |
| if #dims == 1 then | |
| local array = { __length = dims[1] } | |
| for index = 1, dims[1] do | |
| array[index] = array1d[state.index] | |
| state.index = state.index + 1 | |
| end | |
| return array | |
| end | |
| local array = { __length = dims[1] } | |
| local subDims = {} | |
| for index = 2, #dims do | |
| subDims[#subDims + 1] = dims[index] | |
| end | |
| for index = 1, dims[1] do | |
| array[index] = convert1DArrayND(array1d, subDims, state) | |
| end | |
| return array | |
| end | |
| local function newNrbfParser(reader) | |
| return { | |
| reader = reader, | |
| rootId = nil, | |
| headerId = nil, | |
| classById = {}, | |
| objectsById = {}, | |
| binaryLibraries = {}, | |
| deferredMemoryBlocks = {} | |
| } | |
| end | |
| local function readPrimitive(parser, primitiveType) | |
| local reader = parser.reader | |
| if primitiveType == PrimitiveType.Boolean then | |
| return reader:readByte() ~= 0 | |
| elseif primitiveType == PrimitiveType.Byte then | |
| return reader:readByte() | |
| elseif primitiveType == PrimitiveType.Int16 then | |
| return reader:readInt16LE() | |
| elseif primitiveType == PrimitiveType.Int32 then | |
| return reader:readInt32LE() | |
| elseif primitiveType == PrimitiveType.Int64 then | |
| return reader:readInt64LE() | |
| elseif primitiveType == PrimitiveType.SByte then | |
| local value = reader:readByte() | |
| if value >= 128 then | |
| return value - 256 | |
| end | |
| return value | |
| elseif primitiveType == PrimitiveType.UInt16 then | |
| return reader:readUInt16LE() | |
| elseif primitiveType == PrimitiveType.UInt32 then | |
| return reader:readUInt32LE() | |
| elseif primitiveType == PrimitiveType.UInt64 then | |
| return reader:readUInt64LE() | |
| elseif primitiveType == PrimitiveType.String then | |
| return reader:readLengthPrefixedString() | |
| elseif primitiveType == PrimitiveType.Null then | |
| return nil | |
| elseif primitiveType == PrimitiveType.Char then | |
| return reader:readLengthPrefixedString() | |
| end | |
| error("Unsupported NRBF primitive type: " .. tostring(primitiveType), 0) | |
| end | |
| local function readPrimitiveArray(parser, primitiveType, length) | |
| local reader = parser.reader | |
| if primitiveType == PrimitiveType.Byte then | |
| return reader:readExact(length) | |
| end | |
| local array = { __length = length } | |
| for index = 1, length do | |
| array[index] = readPrimitive(parser, primitiveType) | |
| end | |
| return array | |
| end | |
| local function readAdditionalInfo(parser, binaryType) | |
| if binaryType == BinaryType.Primitive or binaryType == BinaryType.PrimitiveArray then | |
| return parser.reader:readByte() | |
| elseif binaryType == BinaryType.String or binaryType == BinaryType.Object | |
| or binaryType == BinaryType.ObjectArray or binaryType == BinaryType.StringArray then | |
| return nil | |
| elseif binaryType == BinaryType.SystemClass then | |
| return parser.reader:readLengthPrefixedString() | |
| elseif binaryType == BinaryType.Class then | |
| return { | |
| className = parser.reader:readLengthPrefixedString(), | |
| libraryId = parser.reader:readInt32LE() | |
| } | |
| end | |
| error("Unsupported NRBF additional info type: " .. tostring(binaryType), 0) | |
| end | |
| local readRecord | |
| local function readObjectArray(parser, length) | |
| local array = { __length = length } | |
| local index = 1 | |
| while index <= length do | |
| local value = readRecord(parser) | |
| if isBinaryLibrary(value) then | |
| elseif isObjectNullMultiple(value) then | |
| index = index + value.count | |
| else | |
| if isReference(value) then | |
| value.parent = array | |
| value.index = index | |
| end | |
| array[index] = value | |
| index = index + 1 | |
| end | |
| end | |
| return array | |
| end | |
| local function readClassInfo(parser, isSystemClass) | |
| local objectId = parser.reader:readInt32LE() | |
| local className = parser.reader:readLengthPrefixedString() | |
| local memberCount = parser.reader:readInt32LE() | |
| local fieldNames = {} | |
| for index = 1, memberCount do | |
| fieldNames[index] = sanitizeIdentifier(parser.reader:readLengthPrefixedString()) | |
| end | |
| local classInfo = createClassInfo(objectId, className, fieldNames, isSystemClass) | |
| parser.classById[objectId] = classInfo | |
| return classInfo | |
| end | |
| local function readMemberTypeInfo(parser, classInfo) | |
| local typeInfo = {} | |
| for index = 1, #classInfo.fieldNames do | |
| typeInfo[index] = { | |
| binaryType = parser.reader:readByte() | |
| } | |
| end | |
| for index = 1, #classInfo.fieldNames do | |
| typeInfo[index].additionalInfo = readAdditionalInfo(parser, typeInfo[index].binaryType) | |
| end | |
| classInfo.typeInfo = typeInfo | |
| end | |
| local function readClassMembers(parser, objectValue, objectId, libraryId) | |
| local fieldCount = #objectValue.__fieldNames | |
| local index = 1 | |
| while index <= fieldCount do | |
| local memberTypeInfo = objectValue.__classInfo.typeInfo and objectValue.__classInfo.typeInfo[index] or nil | |
| local binaryType = memberTypeInfo and memberTypeInfo.binaryType or BinaryType.Object | |
| local additionalInfo = memberTypeInfo and memberTypeInfo.additionalInfo or nil | |
| local value | |
| if binaryType == BinaryType.Primitive then | |
| value = readPrimitive(parser, additionalInfo) | |
| else | |
| value = readRecord(parser) | |
| if isBinaryLibrary(value) then | |
| goto continue | |
| elseif isObjectNullMultiple(value) then | |
| index = index + value.count | |
| goto continue | |
| elseif isReference(value) then | |
| value.parent = objectValue | |
| value.index = index | |
| end | |
| end | |
| setIndexedValue(objectValue, index, value) | |
| index = index + 1 | |
| ::continue:: | |
| end | |
| parser.objectsById[objectId] = objectValue | |
| if libraryId and parser.binaryLibraries[libraryId] then | |
| parser.binaryLibraries[libraryId].objects[objectId] = objectValue | |
| end | |
| if objectValue.__className == "PaintDotNet_MemoryBlock" and objectValue.deferred then | |
| parser.deferredMemoryBlocks[#parser.deferredMemoryBlocks + 1] = objectValue | |
| end | |
| return objectValue | |
| end | |
| readRecord = function(parser) | |
| local recordType = parser.reader:readByte() | |
| if recordType == RecordType.SerializedStreamHeader then | |
| parser.rootId = parser.reader:readInt32LE() | |
| parser.headerId = parser.reader:readInt32LE() | |
| local major = parser.reader:readInt32LE() | |
| local minor = parser.reader:readInt32LE() | |
| if major ~= 1 or minor ~= 0 then | |
| error("Unsupported NRBF serialization version.", 0) | |
| end | |
| return nil | |
| elseif recordType == RecordType.ClassWithId then | |
| local objectId = parser.reader:readInt32LE() | |
| local metadataId = parser.reader:readInt32LE() | |
| local classInfo = parser.classById[metadataId] | |
| if not classInfo then | |
| error("Missing NRBF metadata class: " .. tostring(metadataId), 0) | |
| end | |
| return readClassMembers(parser, createObject(classInfo, objectId), objectId) | |
| elseif recordType == RecordType.SystemClassWithMembers then | |
| local classInfo = readClassInfo(parser, true) | |
| return readClassMembers(parser, createObject(classInfo, classInfo.id), classInfo.id) | |
| elseif recordType == RecordType.ClassWithMembers then | |
| local classInfo = readClassInfo(parser, false) | |
| local libraryId = parser.reader:readInt32LE() | |
| return readClassMembers(parser, createObject(classInfo, classInfo.id), classInfo.id, libraryId) | |
| elseif recordType == RecordType.SystemClassWithMembersAndTypes then | |
| local classInfo = readClassInfo(parser, true) | |
| readMemberTypeInfo(parser, classInfo) | |
| return readClassMembers(parser, createObject(classInfo, classInfo.id), classInfo.id) | |
| elseif recordType == RecordType.ClassWithMembersAndTypes then | |
| local classInfo = readClassInfo(parser, false) | |
| readMemberTypeInfo(parser, classInfo) | |
| local libraryId = parser.reader:readInt32LE() | |
| return readClassMembers(parser, createObject(classInfo, classInfo.id), classInfo.id, libraryId) | |
| elseif recordType == RecordType.BinaryObjectString then | |
| local objectId = parser.reader:readInt32LE() | |
| local value = parser.reader:readLengthPrefixedString() | |
| parser.objectsById[objectId] = value | |
| return value | |
| elseif recordType == RecordType.MemberPrimitiveTyped then | |
| return readPrimitive(parser, parser.reader:readByte()) | |
| elseif recordType == RecordType.BinaryArray then | |
| local objectId = parser.reader:readInt32LE() | |
| local arrayType = parser.reader:readByte() | |
| local rank = parser.reader:readInt32LE() | |
| local lengths = {} | |
| local totalLength = 1 | |
| for index = 1, rank do | |
| lengths[index] = parser.reader:readInt32LE() | |
| totalLength = totalLength * lengths[index] | |
| end | |
| if arrayType == BinaryArrayType.SingleOffset | |
| or arrayType == BinaryArrayType.JaggedOffset | |
| or arrayType == BinaryArrayType.RectangularOffset then | |
| for _ = 1, rank do | |
| parser.reader:readInt32LE() | |
| end | |
| end | |
| local binaryType = parser.reader:readByte() | |
| local additionalInfo = readAdditionalInfo(parser, binaryType) | |
| local array | |
| if binaryType == BinaryType.Primitive then | |
| array = readPrimitiveArray(parser, additionalInfo, totalLength) | |
| else | |
| array = readObjectArray(parser, totalLength) | |
| end | |
| if arrayType == BinaryArrayType.Rectangular or arrayType == BinaryArrayType.RectangularOffset then | |
| array = convert1DArrayND(array, lengths) | |
| end | |
| parser.objectsById[objectId] = array | |
| return array | |
| elseif recordType == RecordType.MemberReference then | |
| return newReference(parser.reader:readInt32LE()) | |
| elseif recordType == RecordType.ObjectNull then | |
| return nil | |
| elseif recordType == RecordType.MessageEnd then | |
| return { __kind = "message-end" } | |
| elseif recordType == RecordType.BinaryLibrary then | |
| local libraryId = parser.reader:readInt32LE() | |
| local name = parser.reader:readLengthPrefixedString() | |
| local library = newBinaryLibrary(libraryId, name) | |
| parser.binaryLibraries[libraryId] = library | |
| return library | |
| elseif recordType == RecordType.ObjectNullMultiple256 then | |
| return newObjectNullMultiple(parser.reader:readByte()) | |
| elseif recordType == RecordType.ObjectNullMultiple then | |
| return newObjectNullMultiple(parser.reader:readInt32LE()) | |
| elseif recordType == RecordType.ArraySinglePrimitive then | |
| local objectId = parser.reader:readInt32LE() | |
| local length = parser.reader:readInt32LE() | |
| local primitiveType = parser.reader:readByte() | |
| local array = readPrimitiveArray(parser, primitiveType, length) | |
| parser.objectsById[objectId] = array | |
| return array | |
| elseif recordType == RecordType.ArraySingleObject or recordType == RecordType.ArraySingleString then | |
| local objectId = parser.reader:readInt32LE() | |
| local length = parser.reader:readInt32LE() | |
| local array = readObjectArray(parser, length) | |
| parser.objectsById[objectId] = array | |
| return array | |
| end | |
| error("Unsupported NRBF record type: " .. tostring(recordType), 0) | |
| end | |
| local function resolveReferences(parser) | |
| for _, objectValue in pairs(parser.objectsById) do | |
| if type(objectValue) == "table" then | |
| local length = getContainerLength(objectValue) | |
| for index = 1, length do | |
| local item = objectValue[index] | |
| if isReference(item) then | |
| local replacement = parser.objectsById[item.objectId] | |
| if replacement == nil then | |
| error("Unresolved NRBF reference: " .. tostring(item.objectId), 0) | |
| end | |
| setIndexedValue(objectValue, index, replacement) | |
| end | |
| end | |
| end | |
| end | |
| end | |
| local function readNrbfGraph(parser) | |
| readRecord(parser) | |
| if parser.rootId == nil then | |
| error("Invalid NRBF stream: missing header.", 0) | |
| end | |
| while true do | |
| local record = readRecord(parser) | |
| if type(record) == "table" and record.__kind == "message-end" then | |
| break | |
| end | |
| end | |
| resolveReferences(parser) | |
| return parser.objectsById[parser.rootId] | |
| end | |
| local function readDeferredMemoryBlock(reader, memoryBlock) | |
| if memoryBlock.hasParent then | |
| error("Unsupported Paint.NET MemoryBlock parent chain.", 0) | |
| end | |
| if not memoryBlock.deferred then | |
| error("Unsupported non-deferred Paint.NET MemoryBlock.", 0) | |
| end | |
| local length = memoryBlock.length64 or memoryBlock.length | |
| if type(length) ~= "number" or length < 0 then | |
| error("Invalid Paint.NET MemoryBlock length.", 0) | |
| end | |
| if length == 0 then | |
| return "" | |
| end | |
| local formatVersion = reader:readByte() | |
| local chunkSize = reader:readUInt32BE() | |
| if chunkSize <= 0 then | |
| error("Invalid Paint.NET chunk size.", 0) | |
| end | |
| local chunkCount = math.floor((length + chunkSize - 1) / chunkSize) | |
| local chunks = {} | |
| for _ = 1, chunkCount do | |
| local chunkNumber = reader:readUInt32BE() | |
| local dataSize = reader:readUInt32BE() | |
| if chunkNumber >= chunkCount then | |
| error("Paint.NET chunk number is out of bounds.", 0) | |
| end | |
| local slot = chunkNumber + 1 | |
| if chunks[slot] ~= nil then | |
| error("Duplicate Paint.NET chunk number encountered.", 0) | |
| end | |
| local rawChunk = reader:readExact(dataSize) | |
| local chunkOffset = chunkNumber * chunkSize | |
| local actualChunkSize = math.min(chunkSize, length - chunkOffset) | |
| local decodedChunk | |
| if formatVersion == 0 then | |
| decodedChunk = gunzipString(rawChunk) | |
| elseif formatVersion == 1 then | |
| decodedChunk = rawChunk | |
| else | |
| error("Unsupported Paint.NET chunk format version: " .. tostring(formatVersion), 0) | |
| end | |
| if #decodedChunk ~= actualChunkSize then | |
| error("Paint.NET chunk decompressed to an unexpected size.", 0) | |
| end | |
| chunks[slot] = decodedChunk | |
| end | |
| for index = 1, chunkCount do | |
| if chunks[index] == nil then | |
| error("Paint.NET chunk data is incomplete.", 0) | |
| end | |
| end | |
| return table.concat(chunks) | |
| end | |
| local LEGACY_BLEND_CLASS_TO_ID = { | |
| PaintDotNet_UserBlendOps_NormalBlendOp = 0, | |
| PaintDotNet_UserBlendOps_MultiplyBlendOp = 1, | |
| PaintDotNet_UserBlendOps_AdditiveBlendOp = 2, | |
| PaintDotNet_UserBlendOps_ColorBurnBlendOp = 3, | |
| PaintDotNet_UserBlendOps_ColorDodgeBlendOp = 4, | |
| PaintDotNet_UserBlendOps_ReflectBlendOp = 5, | |
| PaintDotNet_UserBlendOps_GlowBlendOp = 6, | |
| PaintDotNet_UserBlendOps_OverlayBlendOp = 7, | |
| PaintDotNet_UserBlendOps_DifferenceBlendOp = 8, | |
| PaintDotNet_UserBlendOps_NegationBlendOp = 9, | |
| PaintDotNet_UserBlendOps_LightenBlendOp = 10, | |
| PaintDotNet_UserBlendOps_DarkenBlendOp = 11, | |
| PaintDotNet_UserBlendOps_ScreenBlendOp = 12, | |
| PaintDotNet_UserBlendOps_XorBlendOp = 13 | |
| } | |
| local BLEND_MODE_NAMES = { | |
| [0] = "Normal", | |
| [1] = "Multiply", | |
| [2] = "Additive", | |
| [3] = "Color Burn", | |
| [4] = "Color Dodge", | |
| [5] = "Reflect", | |
| [6] = "Glow", | |
| [7] = "Overlay", | |
| [8] = "Difference", | |
| [9] = "Negation", | |
| [10] = "Lighten", | |
| [11] = "Darken", | |
| [12] = "Screen", | |
| [13] = "XOR" | |
| } | |
| local ASEPRITE_BLEND_MAP = { | |
| [0] = BlendMode.NORMAL, | |
| [1] = BlendMode.MULTIPLY, | |
| [2] = BlendMode.ADDITION, | |
| [3] = BlendMode.COLOR_BURN, | |
| [4] = BlendMode.COLOR_DODGE, | |
| [7] = BlendMode.OVERLAY, | |
| [8] = BlendMode.DIFFERENCE, | |
| [10] = BlendMode.LIGHTEN, | |
| [11] = BlendMode.DARKEN, | |
| [12] = BlendMode.SCREEN | |
| } | |
| local function resolveBlendModeId(bitmapLayer) | |
| local props = bitmapLayer.Layer_properties | |
| if type(props) == "table" and type(props.blendMode) == "table" and type(props.blendMode.value__) == "number" then | |
| return props.blendMode.value__ | |
| end | |
| local legacyProperties = bitmapLayer.properties | |
| local blendOp = legacyProperties and legacyProperties.blendOp | |
| if type(blendOp) == "table" and type(blendOp.__className) == "string" then | |
| return LEGACY_BLEND_CLASS_TO_ID[blendOp.__className] or 0 | |
| end | |
| return 0 | |
| end | |
| local function parsePdnFile(path) | |
| local reader = createBinaryReader(path) | |
| local ok, resultOrError = pcall(function() | |
| local magic = reader:readExact(4) | |
| if magic ~= "PDN3" then | |
| error("Selected file is not a Paint.NET PDN document.", 0) | |
| end | |
| local headerSize = reader:readUInt24LE() | |
| local headerXml = reader:readExact(headerSize) | |
| local marker = reader:readExact(2) | |
| if marker ~= "\0\1" then | |
| error("Unsupported PDN stream marker. Only deferred PDN documents are supported.", 0) | |
| end | |
| local parser = newNrbfParser(reader) | |
| local document = readNrbfGraph(parser) | |
| for _, memoryBlock in ipairs(parser.deferredMemoryBlocks) do | |
| memoryBlock.rawData = readDeferredMemoryBlock(reader, memoryBlock) | |
| end | |
| local width = tonumber(document.width) | |
| local height = tonumber(document.height) | |
| if not width or not height or width < 1 or height < 1 then | |
| error("PDN document has invalid dimensions.", 0) | |
| end | |
| local layersObject = document.layers | |
| local layerItems = layersObject and layersObject.ArrayList__items | |
| local layerCount = layersObject and layersObject.ArrayList__size | |
| if type(layerItems) ~= "table" or type(layerCount) ~= "number" then | |
| error("PDN document layers could not be read.", 0) | |
| end | |
| local warnings = {} | |
| local layers = {} | |
| for index = 1, layerCount do | |
| local layerObject = layerItems[index] | |
| if type(layerObject) ~= "table" then | |
| warnings[#warnings + 1] = string.format("Skipped layer %d because its metadata was empty.", index) | |
| elseif layerObject.__className ~= "PaintDotNet_BitmapLayer" then | |
| warnings[#warnings + 1] = string.format( | |
| "Skipped unsupported layer type on layer %d: %s.", | |
| index, | |
| tostring(layerObject.__className or layerObject.__originalClassName or "Unknown") | |
| ) | |
| else | |
| local props = layerObject.Layer_properties | |
| local surface = layerObject.surface | |
| local scan0 = surface and surface.scan0 | |
| if type(props) ~= "table" or type(surface) ~= "table" or type(scan0) ~= "table" then | |
| warnings[#warnings + 1] = string.format("Skipped layer %d because required bitmap metadata was missing.", index) | |
| elseif tonumber(layerObject.Layer_width) ~= width or tonumber(layerObject.Layer_height) ~= height then | |
| warnings[#warnings + 1] = string.format("Skipped layer %d because its size did not match the document.", index) | |
| elseif type(scan0.rawData) ~= "string" then | |
| warnings[#warnings + 1] = string.format("Skipped layer %d because its pixel buffer could not be reconstructed.", index) | |
| else | |
| layers[#layers + 1] = { | |
| index = index, | |
| name = hasValue(props.name) and props.name or ("Layer " .. index), | |
| visible = props.visible ~= false, | |
| isBackground = props.isBackground == true, | |
| opacity = math.max(0, math.min(255, tonumber(props.opacity) or 255)), | |
| blendModeId = resolveBlendModeId(layerObject), | |
| stride = tonumber(surface.stride) or 0, | |
| rawData = scan0.rawData | |
| } | |
| end | |
| end | |
| end | |
| if #layers == 0 then | |
| error("No supported bitmap layers were found in this PDN document.", 0) | |
| end | |
| return { | |
| width = width, | |
| height = height, | |
| headerXml = headerXml, | |
| layers = layers, | |
| warnings = warnings | |
| } | |
| end) | |
| reader:close() | |
| if not ok then | |
| error(resultOrError, 0) | |
| end | |
| return resultOrError | |
| end | |
| local function convertBgraToRgbaBytes(rawData, width, height, stride) | |
| local visibleStride = width * 4 | |
| local minimumLength = (height - 1) * stride + visibleStride | |
| if stride < visibleStride or #rawData < minimumLength then | |
| error("PDN layer pixel buffer is shorter than expected.", 0) | |
| end | |
| if stride == visibleStride then | |
| return (rawData:gsub("(.)(.)(.)(.)", "%3%2%1%4")) | |
| end | |
| local rows = {} | |
| for rowIndex = 0, height - 1 do | |
| local startIndex = rowIndex * stride + 1 | |
| local row = rawData:sub(startIndex, startIndex + visibleStride - 1) | |
| rows[#rows + 1] = (row:gsub("(.)(.)(.)(.)", "%3%2%1%4")) | |
| end | |
| return table.concat(rows) | |
| end | |
| local function configureImportedLayer(layer, layerInfo, warnings) | |
| layer.name = layerInfo.name | |
| layer.isVisible = layerInfo.visible | |
| local asepriteBlendMode = ASEPRITE_BLEND_MAP[layerInfo.blendModeId] | |
| if asepriteBlendMode ~= nil then | |
| layer.blendMode = asepriteBlendMode | |
| else | |
| layer.blendMode = BlendMode.NORMAL | |
| local originalBlendName = BLEND_MODE_NAMES[layerInfo.blendModeId] or ("Unknown(" .. tostring(layerInfo.blendModeId) .. ")") | |
| layer.data = "Original PDN blend mode: " .. originalBlendName | |
| warnings[#warnings + 1] = string.format( | |
| "Layer \"%s\" used unsupported PDN blend mode \"%s\". Imported as Normal.", | |
| layerInfo.name, | |
| originalBlendName | |
| ) | |
| end | |
| if not layerInfo.isBackground then | |
| layer.opacity = layerInfo.opacity | |
| elseif layerInfo.opacity ~= 255 then | |
| warnings[#warnings + 1] = string.format( | |
| "Background layer \"%s\" had opacity %d. Aseprite background layers are always fully opaque.", | |
| layerInfo.name, | |
| layerInfo.opacity | |
| ) | |
| end | |
| end | |
| local function rebuildImportedPalette(sprite, warnings) | |
| app.activeSprite = sprite | |
| local ok, err = pcall(function() | |
| app.command.ColorQuantization { | |
| ui = false, | |
| withAlpha = true, | |
| maxColors = 256, | |
| useRange = false, | |
| algorithm = "default" | |
| } | |
| end) | |
| if not ok then | |
| warnings[#warnings + 1] = "Could not build a palette from the imported sprite: " .. tostring(err) | |
| end | |
| end | |
| local function importParsedPdn(model, sourcePath) | |
| local sprite = Sprite(model.width, model.height, ColorMode.RGB) | |
| local runOk, runError = pcall(function() | |
| app.transaction(COMMAND_TITLE, function() | |
| local firstLayer = sprite.layers[1] | |
| local firstImportedLayer = nil | |
| local firstImportedInfo = nil | |
| local createdLayerCount = 0 | |
| for _, layerInfo in ipairs(model.layers) do | |
| createdLayerCount = createdLayerCount + 1 | |
| local layer = createdLayerCount == 1 and firstLayer or sprite:newLayer() | |
| if not firstImportedLayer then | |
| firstImportedLayer = layer | |
| firstImportedInfo = layerInfo | |
| end | |
| configureImportedLayer(layer, layerInfo, model.warnings) | |
| local image = Image(model.width, model.height, ColorMode.RGB) | |
| image.bytes = convertBgraToRgbaBytes(layerInfo.rawData, model.width, model.height, layerInfo.stride) | |
| sprite:newCel(layer, 1, image, Point(0, 0)) | |
| end | |
| if firstImportedLayer and firstImportedInfo and firstImportedInfo.isBackground then | |
| app.activeSprite = sprite | |
| app.activeLayer = firstImportedLayer | |
| local backgroundOk, backgroundError = pcall(function() | |
| app.command.BackgroundFromLayer() | |
| end) | |
| if not backgroundOk then | |
| model.warnings[#model.warnings + 1] = "Could not convert the first PDN layer into an Aseprite background layer: " .. tostring(backgroundError) | |
| end | |
| end | |
| sprite.data = "Imported from PDN: " .. app.fs.fileName(sourcePath) | |
| end) | |
| rebuildImportedPalette(sprite, model.warnings) | |
| end) | |
| if not runOk then | |
| pcall(function() | |
| sprite:close() | |
| end) | |
| error(runError, 0) | |
| end | |
| return sprite | |
| end | |
| local function presentWarnings(warnings) | |
| if #warnings == 0 then | |
| return | |
| end | |
| local message = table.concat(warnings, "\n") | |
| if app.isUIAvailable then | |
| app.alert { | |
| title = COMMAND_TITLE .. " warnings", | |
| text = message | |
| } | |
| else | |
| print(message) | |
| end | |
| end | |
| local function resolveImportPath() | |
| local params = app.params or {} | |
| if hasValue(params.file) then | |
| return tostring(params.file) | |
| end | |
| if not app.isUIAvailable then | |
| error("Provide file=<path-to.pdn> when running without the Aseprite UI.", 0) | |
| end | |
| local dlg = Dialog { title = COMMAND_TITLE, resizeable = true } | |
| dlg:file { | |
| id = "file", | |
| label = "Paint.NET:", | |
| title = "Open PDN File", | |
| open = true, | |
| filetypes = { "pdn" }, | |
| entry = true, | |
| hexpand = true, | |
| focus = true | |
| } | |
| dlg:button { id = "ok", text = "Import" } | |
| dlg:button { id = "cancel", text = "Cancel" } | |
| dlg:show() | |
| if not dlg.data.ok then | |
| return nil | |
| end | |
| local filePath = trim(dlg.data.file or "") | |
| if filePath == "" then | |
| error("Please choose a Paint.NET .pdn file to import.", 0) | |
| end | |
| return filePath | |
| end | |
| local function run() | |
| local importPath = resolveImportPath() | |
| if not importPath then | |
| return | |
| end | |
| local ok, resultOrError = pcall(function() | |
| local parsed = parsePdnFile(importPath) | |
| importParsedPdn(parsed, importPath) | |
| return parsed | |
| end) | |
| if not ok then | |
| fail(resultOrError) | |
| return | |
| end | |
| presentWarnings(resultOrError.warnings) | |
| end | |
| run() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Warning
This Gist has been archived. The contents are now maintained here: https://github.com/HaydenReeve/Aseprite-General-Scripts