some fixes
This commit is contained in:
parent
d4f5ff8a99
commit
db7958fcf2
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
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:
|
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
|
# 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
|
# * 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'
|
require 'rmagick'
|
||||||
|
|
||||||
class PixelColor
|
class PixelColor
|
||||||
attr_reader :r, :g, :b
|
attr_reader :r, :g, :b
|
||||||
|
|
||||||
def initialize(r:, g:, b:)
|
def initialize(r:, g:, b:)
|
||||||
@r = r
|
@r = r
|
||||||
@g = g
|
@g = g
|
||||||
@b = b
|
@b = b
|
||||||
end
|
end
|
||||||
|
|
||||||
def ==(other)
|
def ==(other)
|
||||||
r == other.r && g == other.g && b == other.b
|
r == other.r && g == other.g && b == other.b
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_cmap
|
def to_cmap
|
||||||
[
|
[
|
||||||
r * 16 + r,
|
r * 16 + r,
|
||||||
|
@ -35,7 +32,7 @@ class PixelColor
|
||||||
b * 16 + b
|
b * 16 + b
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def inspect
|
def inspect
|
||||||
"RGB: #{r.to_s(16)},#{g.to_s(16)},#{b.to_s(16)}"
|
"RGB: #{r.to_s(16)},#{g.to_s(16)},#{b.to_s(16)}"
|
||||||
end
|
end
|
||||||
|
@ -64,11 +61,11 @@ class Bitplane
|
||||||
def initialize(bits:)
|
def initialize(bits:)
|
||||||
@bits = bits
|
@bits = bits
|
||||||
end
|
end
|
||||||
|
|
||||||
def count
|
def count
|
||||||
@bits.count
|
@bits.count
|
||||||
end
|
end
|
||||||
|
|
||||||
def [](idx)
|
def [](idx)
|
||||||
@bits[idx]
|
@bits[idx]
|
||||||
end
|
end
|
||||||
|
@ -84,38 +81,38 @@ class ImageComponentParser
|
||||||
parser.parse!
|
parser.parse!
|
||||||
parser
|
parser
|
||||||
end
|
end
|
||||||
|
|
||||||
UnknownPaletteColor = Class.new(ArgumentError)
|
UnknownPaletteColor = Class.new(ArgumentError)
|
||||||
|
|
||||||
def initialize(image:, desired_palette:)
|
def initialize(image:, desired_palette:)
|
||||||
@image = image
|
@image = image
|
||||||
@desired_palette = desired_palette
|
@desired_palette = desired_palette
|
||||||
@bitplanes = nil
|
@bitplanes = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def bitplane_count
|
def bitplane_count
|
||||||
Math.sqrt(@desired_palette.length).ceil
|
Math.sqrt(@desired_palette.length).ceil
|
||||||
end
|
end
|
||||||
|
|
||||||
def bitplanes
|
def bitplanes
|
||||||
return @bitplanes if @bitplanes
|
return @bitplanes if @bitplanes
|
||||||
|
|
||||||
bits = []
|
bits = []
|
||||||
# get around duplicate objects
|
# get around duplicate objects
|
||||||
bitplane_count.times { |i| bits << [] }
|
bitplane_count.times { |i| bits << [] }
|
||||||
|
|
||||||
@bytes.each do |byte|
|
@bytes.each do |byte|
|
||||||
mask = 1
|
mask = 1
|
||||||
bitplane_count.times do |bit|
|
bitplane_count.times do |bit|
|
||||||
bits[bit] << (((byte & mask) == mask) ? 1 : 0)
|
bits[bit] << (((byte & mask) == mask) ? 1 : 0)
|
||||||
|
|
||||||
mask = mask << 1
|
mask = mask << 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@bitplanes = bits.map { |b| Bitplane.new(bits: b) }
|
@bitplanes = bits.map { |b| Bitplane.new(bits: b) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse!
|
def parse!
|
||||||
@bytes = []
|
@bytes = []
|
||||||
@image.each_pixel do |px|
|
@image.each_pixel do |px|
|
||||||
|
@ -125,12 +122,12 @@ class ImageComponentParser
|
||||||
g: px.green >> 12,
|
g: px.green >> 12,
|
||||||
b: px.blue >> 12
|
b: px.blue >> 12
|
||||||
)
|
)
|
||||||
|
|
||||||
index = @desired_palette.find_index { |p| p == pixel_color }
|
index = @desired_palette.find_index { |p| p == pixel_color }
|
||||||
unless index
|
unless index
|
||||||
raise UnknownPaletteColor, "Palette color not found! Check export: #{pixel_color}"
|
raise UnknownPaletteColor, "Palette color not found! Check export: #{pixel_color}"
|
||||||
end
|
end
|
||||||
|
|
||||||
@bytes << index
|
@bytes << index
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -141,20 +138,20 @@ module IFF
|
||||||
def to_bytes
|
def to_bytes
|
||||||
data_bytes = to_data_bytes
|
data_bytes = to_data_bytes
|
||||||
data_bytes_count = data_bytes.count
|
data_bytes_count = data_bytes.count
|
||||||
|
|
||||||
output = self.class.name.split("::").last.unpack("C*") +
|
output = self.class.name.split("::").last.unpack("C*") +
|
||||||
[data_bytes_count].pack("N").unpack("C*") +
|
[data_bytes_count].pack("N").unpack("C*") +
|
||||||
data_bytes
|
data_bytes
|
||||||
|
|
||||||
# pad chunk
|
# pad chunk
|
||||||
output << 0 if data_bytes_count.odd?
|
output << 0 if data_bytes_count.odd?
|
||||||
|
|
||||||
output
|
output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Construct an IFF ILBM image from the provided data.
|
# Construct an IFF ILBM image from the provided data.
|
||||||
class ILBMBuilder
|
class ILBMBuilder
|
||||||
def initialize(
|
def initialize(
|
||||||
width:,
|
width:,
|
||||||
height:,
|
height:,
|
||||||
|
@ -170,28 +167,28 @@ module IFF
|
||||||
@row_writer = row_writer
|
@row_writer = row_writer
|
||||||
@screen_format = screen_format
|
@screen_format = screen_format
|
||||||
end
|
end
|
||||||
|
|
||||||
def row_writer
|
def row_writer
|
||||||
return UncompressedRowWriter
|
return UncompressedRowWriter
|
||||||
|
|
||||||
# here's no reason to use compression if the image is less
|
# here's no reason to use compression if the image is less
|
||||||
# than 24 pixels wide
|
# than 24 pixels wide
|
||||||
if @row_writer == CompressedRowWriter && @width < 24
|
if @row_writer == CompressedRowWriter && @width < 24
|
||||||
return UncompressedRowWriter
|
return UncompressedRowWriter
|
||||||
end
|
end
|
||||||
|
|
||||||
@row_writer
|
@row_writer
|
||||||
end
|
end
|
||||||
|
|
||||||
def build
|
def build
|
||||||
form = FORM.new
|
form = FORM.new
|
||||||
|
|
||||||
bmhd = BMHD.new
|
bmhd = BMHD.new
|
||||||
bmhd.width = @width
|
bmhd.width = @width
|
||||||
bmhd.height = @height
|
bmhd.height = @height
|
||||||
bmhd.planes = Math.sqrt(@palette.length).ceil
|
bmhd.planes = Math.sqrt(@palette.length).ceil
|
||||||
bmhd.compression = row_writer.bmhd_flag
|
bmhd.compression = row_writer.bmhd_flag
|
||||||
|
|
||||||
cmap = CMAP.new(palette: @palette)
|
cmap = CMAP.new(palette: @palette)
|
||||||
|
|
||||||
body = BODY.new(
|
body = BODY.new(
|
||||||
|
@ -199,62 +196,62 @@ module IFF
|
||||||
width: @width,
|
width: @width,
|
||||||
row_writer: row_writer
|
row_writer: row_writer
|
||||||
)
|
)
|
||||||
|
|
||||||
ilbm = ILBM.new
|
ilbm = ILBM.new
|
||||||
ilbm.bmhd = bmhd
|
ilbm.bmhd = bmhd
|
||||||
ilbm.cmap = cmap
|
ilbm.cmap = cmap
|
||||||
ilbm.body = body
|
ilbm.body = body
|
||||||
ilbm.camg = @screen_format
|
ilbm.camg = @screen_format
|
||||||
|
|
||||||
form << ilbm
|
form << ilbm
|
||||||
|
|
||||||
form
|
form
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# IFF wrapper
|
# IFF wrapper
|
||||||
class FORM
|
class FORM
|
||||||
include WithHeader
|
include WithHeader
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@chunks = []
|
@chunks = []
|
||||||
end
|
end
|
||||||
|
|
||||||
def <<(chunk)
|
def <<(chunk)
|
||||||
@chunks << chunk
|
@chunks << chunk
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_data_bytes
|
def to_data_bytes
|
||||||
@chunks.flat_map(&:to_bytes)
|
@chunks.flat_map(&:to_bytes)
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
to_bytes.pack("C*")
|
to_bytes.pack("C*")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Image type header
|
# Image type header
|
||||||
class ILBM
|
class ILBM
|
||||||
attr_accessor :bmhd, :cmap, :body, :camg
|
attr_accessor :bmhd, :cmap, :body, :camg
|
||||||
|
|
||||||
def to_bytes
|
def to_bytes
|
||||||
"ILBM".unpack("C*") +
|
"ILBM".unpack("C*") +
|
||||||
bmhd.to_bytes +
|
bmhd.to_bytes +
|
||||||
cmap.to_bytes +
|
cmap.to_bytes +
|
||||||
camg.to_bytes +
|
camg.to_bytes +
|
||||||
# body is always last
|
# body is always last
|
||||||
body.to_bytes
|
body.to_bytes
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Image properties
|
# Image properties
|
||||||
class BMHD
|
class BMHD
|
||||||
include WithHeader
|
include WithHeader
|
||||||
|
|
||||||
ASPECT_RATIO = 0x2C
|
ASPECT_RATIO = 0x2C
|
||||||
|
|
||||||
attr_accessor :width, :height, :planes, :compression
|
attr_accessor :width, :height, :planes, :compression
|
||||||
|
|
||||||
def to_data_bytes
|
def to_data_bytes
|
||||||
[
|
[
|
||||||
*word_to_bytes(width),
|
*word_to_bytes(width),
|
||||||
|
@ -271,14 +268,14 @@ module IFF
|
||||||
*word_to_bytes(height),
|
*word_to_bytes(height),
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def word_to_bytes(word)
|
def word_to_bytes(word)
|
||||||
[ word >> 8, word & 0xff ]
|
[ word >> 8, word & 0xff ]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Color map
|
# Color map
|
||||||
class CMAP
|
class CMAP
|
||||||
include WithHeader
|
include WithHeader
|
||||||
|
@ -286,12 +283,12 @@ module IFF
|
||||||
def initialize(palette:)
|
def initialize(palette:)
|
||||||
@palette = palette
|
@palette = palette
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_data_bytes
|
def to_data_bytes
|
||||||
@palette.flat_map(&:to_cmap)
|
@palette.flat_map(&:to_cmap)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Amiga-related display properties
|
# Amiga-related display properties
|
||||||
class CAMG
|
class CAMG
|
||||||
include WithHeader
|
include WithHeader
|
||||||
|
@ -299,16 +296,16 @@ module IFF
|
||||||
def self.PalLowRes
|
def self.PalLowRes
|
||||||
new(0x00021000)
|
new(0x00021000)
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(setting)
|
def initialize(setting)
|
||||||
@setting = setting
|
@setting = setting
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_data_bytes
|
def to_data_bytes
|
||||||
[@setting].pack("N").unpack("C*")
|
[@setting].pack("N").unpack("C*")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module AsRowWriter
|
module AsRowWriter
|
||||||
def initialize
|
def initialize
|
||||||
@data = []
|
@data = []
|
||||||
|
@ -317,28 +314,28 @@ module IFF
|
||||||
def <<(byte)
|
def <<(byte)
|
||||||
@data << byte
|
@data << byte
|
||||||
end
|
end
|
||||||
|
|
||||||
# Output pixel data is word padded
|
# Output pixel data is word padded
|
||||||
def ensure_padded!
|
def ensure_padded!
|
||||||
@data << 0 if @data.length.odd?
|
@data << 0 if @data.length.odd?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Write out uncompressed bitplane data
|
# Write out uncompressed bitplane data
|
||||||
class UncompressedRowWriter
|
class UncompressedRowWriter
|
||||||
include AsRowWriter
|
include AsRowWriter
|
||||||
|
|
||||||
def self.bmhd_flag
|
def self.bmhd_flag
|
||||||
0
|
0
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_bytes
|
def to_bytes
|
||||||
ensure_padded!
|
ensure_padded!
|
||||||
|
|
||||||
@data
|
@data
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Write out compressed bitplane data using the
|
# Write out compressed bitplane data using the
|
||||||
# ByteRun1 run encoding algorithm.
|
# ByteRun1 run encoding algorithm.
|
||||||
class CompressedRowWriter
|
class CompressedRowWriter
|
||||||
|
@ -347,12 +344,12 @@ module IFF
|
||||||
def self.bmhd_flag
|
def self.bmhd_flag
|
||||||
1
|
1
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_bytes
|
def to_bytes
|
||||||
ensure_padded!
|
ensure_padded!
|
||||||
|
|
||||||
runs = []
|
runs = []
|
||||||
|
|
||||||
# discover initial runs
|
# discover initial runs
|
||||||
# format is [byte, run count]
|
# format is [byte, run count]
|
||||||
@data.each do |byte|
|
@data.each do |byte|
|
||||||
|
@ -377,11 +374,11 @@ module IFF
|
||||||
if out.last&.first != :literal
|
if out.last&.first != :literal
|
||||||
out << [:literal]
|
out << [:literal]
|
||||||
end
|
end
|
||||||
|
|
||||||
out.last << byte
|
out.last << byte
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# write out compressed data
|
# write out compressed data
|
||||||
runs.each_with_object([]) do |type, obj|
|
runs.each_with_object([]) do |type, obj|
|
||||||
case type.first
|
case type.first
|
||||||
|
@ -397,56 +394,56 @@ module IFF
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Pixel data
|
# Pixel data
|
||||||
class BODY
|
class BODY
|
||||||
include WithHeader
|
include WithHeader
|
||||||
|
|
||||||
def initialize(bitplanes:, width:, row_writer:)
|
def initialize(bitplanes:, width:, row_writer:)
|
||||||
@bitplanes = bitplanes
|
@bitplanes = bitplanes
|
||||||
@width = width
|
@width = width
|
||||||
@row_writer = row_writer
|
@row_writer = row_writer
|
||||||
end
|
end
|
||||||
|
|
||||||
START_MASK = 1 << 7
|
START_MASK = 1 << 7
|
||||||
|
|
||||||
def to_data_bytes
|
def to_data_bytes
|
||||||
rows = []
|
rows = []
|
||||||
|
|
||||||
row_count = @bitplanes[0].count / @width
|
row_count = @bitplanes[0].count / @width
|
||||||
|
|
||||||
row_count.times do |y|
|
row_count.times do |y|
|
||||||
# interleave the bitplanes
|
# interleave the bitplanes
|
||||||
pos = y * @width
|
pos = y * @width
|
||||||
|
|
||||||
@bitplanes.each do |bitplane|
|
@bitplanes.each do |bitplane|
|
||||||
val = 0
|
val = 0
|
||||||
mask = START_MASK
|
mask = START_MASK
|
||||||
|
|
||||||
# the row writer takes care of turning bitplane data into output
|
# the row writer takes care of turning bitplane data into output
|
||||||
current_row_writer = @row_writer.new
|
current_row_writer = @row_writer.new
|
||||||
|
|
||||||
@width.times do |x|
|
@width.times do |x|
|
||||||
val = bitplane[pos + x] * mask | val
|
val = bitplane[pos + x] * mask | val
|
||||||
mask >>= 1
|
mask >>= 1
|
||||||
|
|
||||||
# end of byte
|
# end of byte
|
||||||
if mask == 0
|
if mask == 0
|
||||||
current_row_writer << val
|
current_row_writer << val
|
||||||
|
|
||||||
val = 0
|
val = 0
|
||||||
mask = START_MASK
|
mask = START_MASK
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
unless mask == START_MASK
|
unless mask == START_MASK
|
||||||
current_row_writer << val
|
current_row_writer << val
|
||||||
end
|
end
|
||||||
|
|
||||||
rows.concat(current_row_writer.to_bytes)
|
rows.concat(current_row_writer.to_bytes)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
rows
|
rows
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue