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

View File

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

View File

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