some fixes
This commit is contained in:
parent
d4f5ff8a99
commit
db7958fcf2
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 TheIndustriousRabbit
|
||||
Copyright (c) 2023 John Bintz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
# png-to-ilbm
|
||||
|
||||
Convert PNG images to IFF ILBM images using Ruby.
|
||||
Convert PNG images to IFF ILBM images using Ruby, preserving palette indexes.
|
||||
|
||||
Copyright 2023 John Bintz. Learn more at https://theindustriousrabbit.com/
|
||||
|
||||
Licensed under the MIT License.
|
||||
|
|
|
@ -10,24 +10,21 @@
|
|||
#
|
||||
# * http://etwright.org/lwsdk/docs/filefmts/ilbm.html
|
||||
|
||||
# * [ ] AMOS and ADPro fail on an image that's less than 8 pixels wide?
|
||||
# * DPaint can load it, other apps get weird
|
||||
|
||||
require 'rmagick'
|
||||
|
||||
class PixelColor
|
||||
attr_reader :r, :g, :b
|
||||
|
||||
|
||||
def initialize(r:, g:, b:)
|
||||
@r = r
|
||||
@g = g
|
||||
@b = b
|
||||
end
|
||||
|
||||
|
||||
def ==(other)
|
||||
r == other.r && g == other.g && b == other.b
|
||||
end
|
||||
|
||||
|
||||
def to_cmap
|
||||
[
|
||||
r * 16 + r,
|
||||
|
@ -35,7 +32,7 @@ class PixelColor
|
|||
b * 16 + b
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
def inspect
|
||||
"RGB: #{r.to_s(16)},#{g.to_s(16)},#{b.to_s(16)}"
|
||||
end
|
||||
|
@ -64,11 +61,11 @@ class Bitplane
|
|||
def initialize(bits:)
|
||||
@bits = bits
|
||||
end
|
||||
|
||||
|
||||
def count
|
||||
@bits.count
|
||||
end
|
||||
|
||||
|
||||
def [](idx)
|
||||
@bits[idx]
|
||||
end
|
||||
|
@ -84,38 +81,38 @@ class ImageComponentParser
|
|||
parser.parse!
|
||||
parser
|
||||
end
|
||||
|
||||
|
||||
UnknownPaletteColor = Class.new(ArgumentError)
|
||||
|
||||
|
||||
def initialize(image:, desired_palette:)
|
||||
@image = image
|
||||
@desired_palette = desired_palette
|
||||
@bitplanes = nil
|
||||
end
|
||||
|
||||
|
||||
def bitplane_count
|
||||
Math.sqrt(@desired_palette.length).ceil
|
||||
end
|
||||
|
||||
|
||||
def bitplanes
|
||||
return @bitplanes if @bitplanes
|
||||
|
||||
|
||||
bits = []
|
||||
# get around duplicate objects
|
||||
bitplane_count.times { |i| bits << [] }
|
||||
|
||||
|
||||
@bytes.each do |byte|
|
||||
mask = 1
|
||||
bitplane_count.times do |bit|
|
||||
bits[bit] << (((byte & mask) == mask) ? 1 : 0)
|
||||
|
||||
|
||||
mask = mask << 1
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@bitplanes = bits.map { |b| Bitplane.new(bits: b) }
|
||||
end
|
||||
|
||||
|
||||
def parse!
|
||||
@bytes = []
|
||||
@image.each_pixel do |px|
|
||||
|
@ -125,12 +122,12 @@ class ImageComponentParser
|
|||
g: px.green >> 12,
|
||||
b: px.blue >> 12
|
||||
)
|
||||
|
||||
|
||||
index = @desired_palette.find_index { |p| p == pixel_color }
|
||||
unless index
|
||||
raise UnknownPaletteColor, "Palette color not found! Check export: #{pixel_color}"
|
||||
end
|
||||
|
||||
|
||||
@bytes << index
|
||||
end
|
||||
end
|
||||
|
@ -141,20 +138,20 @@ module IFF
|
|||
def to_bytes
|
||||
data_bytes = to_data_bytes
|
||||
data_bytes_count = data_bytes.count
|
||||
|
||||
|
||||
output = self.class.name.split("::").last.unpack("C*") +
|
||||
[data_bytes_count].pack("N").unpack("C*") +
|
||||
data_bytes
|
||||
|
||||
[data_bytes_count].pack("N").unpack("C*") +
|
||||
data_bytes
|
||||
|
||||
# pad chunk
|
||||
output << 0 if data_bytes_count.odd?
|
||||
|
||||
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Construct an IFF ILBM image from the provided data.
|
||||
class ILBMBuilder
|
||||
class ILBMBuilder
|
||||
def initialize(
|
||||
width:,
|
||||
height:,
|
||||
|
@ -170,28 +167,28 @@ module IFF
|
|||
@row_writer = row_writer
|
||||
@screen_format = screen_format
|
||||
end
|
||||
|
||||
|
||||
def row_writer
|
||||
return UncompressedRowWriter
|
||||
|
||||
|
||||
# here's no reason to use compression if the image is less
|
||||
# than 24 pixels wide
|
||||
if @row_writer == CompressedRowWriter && @width < 24
|
||||
return UncompressedRowWriter
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@row_writer
|
||||
end
|
||||
|
||||
|
||||
def build
|
||||
form = FORM.new
|
||||
|
||||
|
||||
bmhd = BMHD.new
|
||||
bmhd.width = @width
|
||||
bmhd.height = @height
|
||||
bmhd.planes = Math.sqrt(@palette.length).ceil
|
||||
bmhd.compression = row_writer.bmhd_flag
|
||||
|
||||
|
||||
cmap = CMAP.new(palette: @palette)
|
||||
|
||||
body = BODY.new(
|
||||
|
@ -199,62 +196,62 @@ module IFF
|
|||
width: @width,
|
||||
row_writer: row_writer
|
||||
)
|
||||
|
||||
|
||||
ilbm = ILBM.new
|
||||
ilbm.bmhd = bmhd
|
||||
ilbm.cmap = cmap
|
||||
ilbm.body = body
|
||||
ilbm.camg = @screen_format
|
||||
|
||||
|
||||
form << ilbm
|
||||
|
||||
|
||||
form
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# IFF wrapper
|
||||
class FORM
|
||||
include WithHeader
|
||||
|
||||
|
||||
def initialize
|
||||
@chunks = []
|
||||
end
|
||||
|
||||
|
||||
def <<(chunk)
|
||||
@chunks << chunk
|
||||
end
|
||||
|
||||
|
||||
def to_data_bytes
|
||||
@chunks.flat_map(&:to_bytes)
|
||||
end
|
||||
|
||||
|
||||
def to_s
|
||||
to_bytes.pack("C*")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Image type header
|
||||
class ILBM
|
||||
attr_accessor :bmhd, :cmap, :body, :camg
|
||||
|
||||
|
||||
def to_bytes
|
||||
"ILBM".unpack("C*") +
|
||||
bmhd.to_bytes +
|
||||
cmap.to_bytes +
|
||||
camg.to_bytes +
|
||||
# body is always last
|
||||
body.to_bytes
|
||||
bmhd.to_bytes +
|
||||
cmap.to_bytes +
|
||||
camg.to_bytes +
|
||||
# body is always last
|
||||
body.to_bytes
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Image properties
|
||||
class BMHD
|
||||
include WithHeader
|
||||
|
||||
|
||||
ASPECT_RATIO = 0x2C
|
||||
|
||||
|
||||
attr_accessor :width, :height, :planes, :compression
|
||||
|
||||
|
||||
def to_data_bytes
|
||||
[
|
||||
*word_to_bytes(width),
|
||||
|
@ -271,14 +268,14 @@ module IFF
|
|||
*word_to_bytes(height),
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
|
||||
def word_to_bytes(word)
|
||||
[ word >> 8, word & 0xff ]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Color map
|
||||
class CMAP
|
||||
include WithHeader
|
||||
|
@ -286,12 +283,12 @@ module IFF
|
|||
def initialize(palette:)
|
||||
@palette = palette
|
||||
end
|
||||
|
||||
|
||||
def to_data_bytes
|
||||
@palette.flat_map(&:to_cmap)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Amiga-related display properties
|
||||
class CAMG
|
||||
include WithHeader
|
||||
|
@ -299,16 +296,16 @@ module IFF
|
|||
def self.PalLowRes
|
||||
new(0x00021000)
|
||||
end
|
||||
|
||||
|
||||
def initialize(setting)
|
||||
@setting = setting
|
||||
end
|
||||
|
||||
|
||||
def to_data_bytes
|
||||
[@setting].pack("N").unpack("C*")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
module AsRowWriter
|
||||
def initialize
|
||||
@data = []
|
||||
|
@ -317,28 +314,28 @@ module IFF
|
|||
def <<(byte)
|
||||
@data << byte
|
||||
end
|
||||
|
||||
|
||||
# Output pixel data is word padded
|
||||
def ensure_padded!
|
||||
@data << 0 if @data.length.odd?
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Write out uncompressed bitplane data
|
||||
class UncompressedRowWriter
|
||||
include AsRowWriter
|
||||
|
||||
|
||||
def self.bmhd_flag
|
||||
0
|
||||
end
|
||||
|
||||
|
||||
def to_bytes
|
||||
ensure_padded!
|
||||
|
||||
|
||||
@data
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Write out compressed bitplane data using the
|
||||
# ByteRun1 run encoding algorithm.
|
||||
class CompressedRowWriter
|
||||
|
@ -347,12 +344,12 @@ module IFF
|
|||
def self.bmhd_flag
|
||||
1
|
||||
end
|
||||
|
||||
|
||||
def to_bytes
|
||||
ensure_padded!
|
||||
|
||||
runs = []
|
||||
|
||||
|
||||
# discover initial runs
|
||||
# format is [byte, run count]
|
||||
@data.each do |byte|
|
||||
|
@ -377,11 +374,11 @@ module IFF
|
|||
if out.last&.first != :literal
|
||||
out << [:literal]
|
||||
end
|
||||
|
||||
|
||||
out.last << byte
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# write out compressed data
|
||||
runs.each_with_object([]) do |type, obj|
|
||||
case type.first
|
||||
|
@ -397,56 +394,56 @@ module IFF
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Pixel data
|
||||
class BODY
|
||||
include WithHeader
|
||||
|
||||
|
||||
def initialize(bitplanes:, width:, row_writer:)
|
||||
@bitplanes = bitplanes
|
||||
@width = width
|
||||
@row_writer = row_writer
|
||||
end
|
||||
|
||||
|
||||
START_MASK = 1 << 7
|
||||
|
||||
|
||||
def to_data_bytes
|
||||
rows = []
|
||||
|
||||
|
||||
row_count = @bitplanes[0].count / @width
|
||||
|
||||
|
||||
row_count.times do |y|
|
||||
# interleave the bitplanes
|
||||
pos = y * @width
|
||||
|
||||
@bitplanes.each do |bitplane|
|
||||
|
||||
@bitplanes.each do |bitplane|
|
||||
val = 0
|
||||
mask = START_MASK
|
||||
|
||||
|
||||
# the row writer takes care of turning bitplane data into output
|
||||
current_row_writer = @row_writer.new
|
||||
|
||||
|
||||
@width.times do |x|
|
||||
val = bitplane[pos + x] * mask | val
|
||||
mask >>= 1
|
||||
|
||||
|
||||
# end of byte
|
||||
if mask == 0
|
||||
current_row_writer << val
|
||||
|
||||
|
||||
val = 0
|
||||
mask = START_MASK
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
unless mask == START_MASK
|
||||
current_row_writer << val
|
||||
end
|
||||
|
||||
|
||||
rows.concat(current_row_writer.to_bytes)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
rows
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue