some fixes

This commit is contained in:
John Bintz 2023-10-29 08:58:39 -04:00
parent d4f5ff8a99
commit db7958fcf2
3 changed files with 89 additions and 88 deletions

View File

@ -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:

View File

@ -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.

View File

@ -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