414 lines
12 KiB
C++
414 lines
12 KiB
C++
/*
|
|
* Bitmap to (Sierra) AGI picture resource converter
|
|
* Copyright (C) 2012 Jarno Elonen
|
|
*
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
*
|
|
*/
|
|
|
|
#include "bmp2agipic.h"
|
|
|
|
#include <QColor>
|
|
|
|
#include <cmath>
|
|
#include <queue>
|
|
#include <vector>
|
|
|
|
#include <cstdio>
|
|
#include <cassert>
|
|
|
|
static const int AGI_WIDTH = 160;
|
|
static const int AGI_HEIGHT = 168;
|
|
static const int N_COLORS = 16;
|
|
static const unsigned char COLOR_NONE = 255;
|
|
|
|
const QColor ega[] = {
|
|
QColor(0x00, 0x00, 0x00), // black
|
|
QColor(0x00, 0x00, 0xA0), // blue
|
|
QColor(0x00, 0xA0, 0x00), // green
|
|
QColor(0x00, 0xA0, 0xA0), // cyan
|
|
QColor(0xA0, 0x00, 0x00), // red
|
|
QColor(0xA0, 0x00, 0xA0), // magenta
|
|
QColor(0xA0, 0x50, 0x00), // brown
|
|
QColor(0xA0, 0xA0, 0xA0), // light grey
|
|
QColor(0x50, 0x50, 0x50), // dark grey
|
|
QColor(0x50, 0x50, 0xFF), // light blue
|
|
QColor(0x50, 0xFF, 0x50), // light green
|
|
QColor(0x50, 0xFF, 0xFF), // light cyan
|
|
QColor(0xFF, 0x50, 0x50), // light red
|
|
QColor(0xFF, 0x50, 0xFF), // light magenta
|
|
QColor(0xFF, 0xFF, 0x50), // yellow
|
|
QColor(0xFF, 0xFF, 0xFF) // white
|
|
};
|
|
|
|
typedef unsigned char AGIPic[AGI_WIDTH][AGI_HEIGHT];
|
|
|
|
struct Coord {
|
|
Coord( int _x, int _y ) : x(_x), y(_y) {}
|
|
int x, y;
|
|
};
|
|
|
|
struct CoordColor {
|
|
CoordColor( int _x, int _y, unsigned char _c ) : x(_x), y(_y), c(_c) {}
|
|
int x, y;
|
|
unsigned char c;
|
|
};
|
|
|
|
|
|
|
|
// Quantize given image to EGA palette and reshape to AGI native AGI_WIDTHxAGI_HEIGHT
|
|
void QuantizeAGI( const QImage& img, AGIPic& out )
|
|
{
|
|
#define SQUARE(x) ((x)*(x))
|
|
|
|
assert(img.width() == AGI_WIDTH || img.width() == AGI_WIDTH*2 );
|
|
assert(img.height() >= AGI_HEIGHT );
|
|
|
|
int xstep = (img.width() == AGI_WIDTH) ? 1 : 2;
|
|
for ( int y=0; y<AGI_HEIGHT; ++y )
|
|
{
|
|
for ( int x=0; x<img.width(); x+=xstep )
|
|
{
|
|
float selErr = -1.f;
|
|
unsigned char sel = 0;
|
|
for ( int c=0; c<N_COLORS; ++c )
|
|
{
|
|
QColor pix( img.pixel(x,y) );
|
|
float err = sqrtf(
|
|
SQUARE(ega[c].redF() - pix.redF()) +
|
|
SQUARE(ega[c].greenF() - pix.greenF()) +
|
|
SQUARE(ega[c].blueF() - pix.blueF()));
|
|
if ( selErr < 0.f || err < selErr )
|
|
{
|
|
sel = c;
|
|
selErr =err;
|
|
}
|
|
}
|
|
out[x/xstep][y] = sel;
|
|
}
|
|
}
|
|
}
|
|
|
|
unsigned char& agiPix( AGIPic& pic, int x, int y )
|
|
{
|
|
static unsigned char dummy = COLOR_NONE;
|
|
assert(dummy == COLOR_NONE); // not been accidentally overwritten...
|
|
if ( x<0 || x>=AGI_WIDTH || y<0 || y>=AGI_HEIGHT )
|
|
return dummy;
|
|
else
|
|
return pic[x][y];
|
|
}
|
|
|
|
// Returns true if neighborhood is solid, suitable for floodfill
|
|
bool isOnFloodFillArea( AGIPic& pic, int x, int y )
|
|
{
|
|
if ( agiPix(pic, x,y) == COLOR_NONE )
|
|
return false;
|
|
for ( int dx=-1; dx<=1; ++dx )
|
|
for ( int dy=-1; dy<=1; ++dy )
|
|
if ( agiPix(pic, x,y) != agiPix(pic, x+dx,y+dy))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool has4NeighborOfColor( AGIPic& pic, int x, int y, unsigned char c )
|
|
{
|
|
return \
|
|
(x<AGI_WIDTH-1 && agiPix(pic,x+1,y+0) == c) ||
|
|
(x>0 && agiPix(pic,x-1,y+0) == c) ||
|
|
(y<AGI_HEIGHT-1 && agiPix(pic,x+0,y+1) == c) ||
|
|
(y>0 && agiPix(pic,x+0,y-1) == c);
|
|
}
|
|
|
|
// Trace how long a line continues to given direction (excluding starting pixel)
|
|
int traceToDir( AGIPic& pic, int x, int y, int dx, int dy, unsigned char c, int max )
|
|
{
|
|
assert(c != COLOR_NONE); // could cause infinite trace
|
|
int cnt = -1;
|
|
do
|
|
{
|
|
cnt++;
|
|
if ( cnt >= max )
|
|
break;
|
|
x+=dx;
|
|
y+=dy;
|
|
} while ( agiPix(pic,x, y) == c );
|
|
return cnt;
|
|
}
|
|
|
|
int count8NeighborOfColor( AGIPic& pic, int x, int y, unsigned char c )
|
|
{
|
|
int res = 0;
|
|
for ( int dx=-1; dx<=1; ++dx )
|
|
for ( int dy=-1; dy<=1; ++dy )
|
|
if ( dx!=0 || dy!=0 )
|
|
if ( agiPix(pic,x+dx,y+dy) == c )
|
|
res++;
|
|
return res;
|
|
}
|
|
|
|
|
|
// Flood fill area with color COLOR_NONE, but
|
|
// leave border pixels of previous COLOR_NONE areas
|
|
// untouched so that we'll get proper fill borders later.
|
|
void floodFillEmpty( AGIPic& pic, int x, int y )
|
|
{
|
|
std::queue<Coord> q;
|
|
q.push(Coord(x,y));
|
|
|
|
const unsigned char visited = COLOR_NONE-1;
|
|
const unsigned char src_color = pic[x][y];
|
|
assert( src_color != COLOR_NONE );
|
|
assert( src_color != visited );
|
|
|
|
while ( !q.empty())
|
|
{
|
|
Coord c = q.front();
|
|
q.pop();
|
|
if ( agiPix(pic, c.x, c.y) == src_color &&
|
|
!has4NeighborOfColor(pic,c.x,c.y, COLOR_NONE ))
|
|
{
|
|
agiPix(pic, c.x, c.y) = visited;
|
|
q.push(Coord(c.x+1, c.y+0));
|
|
q.push(Coord(c.x-1, c.y+0));
|
|
q.push(Coord(c.x+0, c.y+1));
|
|
q.push(Coord(c.x+0, c.y-1));
|
|
}
|
|
}
|
|
|
|
for (int y=0; y<AGI_HEIGHT; ++y )
|
|
for (int x=0; x<AGI_WIDTH; ++x )
|
|
if ( agiPix(pic,x,y) == visited )
|
|
agiPix(pic,x,y) = COLOR_NONE;
|
|
}
|
|
|
|
int findBestLineStartFromArea( AGIPic& pic, int x0, int y0, int w, int h, int color, std::vector<Coord>& singlePoints, Coord& minCoord )
|
|
{
|
|
// Find pixel with the least neighbors of color i
|
|
// (i.e. most probable end of line)
|
|
int minCount = 0xFF;
|
|
|
|
for (int y=y0; y<y0+h; ++y )
|
|
for (int x=x0; x<x0+w; ++x )
|
|
{
|
|
if ( agiPix(pic,x,y) == color )
|
|
{
|
|
int cnt = count8NeighborOfColor( pic,x,y, agiPix(pic,x,y) );
|
|
if ( cnt == 0 ) // lonely pixel, handle immediately
|
|
{
|
|
singlePoints.push_back(Coord(x,y));
|
|
agiPix(pic,x,y) = COLOR_NONE;
|
|
}
|
|
else if ( cnt < minCount ) // line
|
|
{
|
|
minCount = cnt;
|
|
minCoord.x = x;
|
|
minCoord.y = y;
|
|
}
|
|
}
|
|
}
|
|
return minCount;
|
|
}
|
|
|
|
void replaceLines( AGIPic& pic, QByteArray* res, bool isPri )
|
|
{
|
|
// Handle one color at a time
|
|
for (int color=0; color<N_COLORS; ++color )
|
|
{
|
|
// Check that there's at least one pixel of color i left
|
|
for (int y=0; y<AGI_HEIGHT; ++y )
|
|
for (int x=0; x<AGI_WIDTH; ++x )
|
|
if ( agiPix(pic,x,y) == color )
|
|
goto found;
|
|
continue;
|
|
found:
|
|
*res += char( isPri ? 0xF2 : 0xF0 ); // Change color command
|
|
*res += char( color ); // Color index
|
|
|
|
std::vector<Coord> singlePoints;
|
|
|
|
while (true) // loop until no pixels of color i are found
|
|
{
|
|
Coord minCoord(-1,-1);
|
|
int minCount = findBestLineStartFromArea( pic, 0,0, AGI_WIDTH,AGI_HEIGHT, color, singlePoints, minCoord );
|
|
if ( minCount == 0xFF ) // No more lines found
|
|
break;
|
|
|
|
newTrace:
|
|
// Start tracing the line
|
|
int x=minCoord.x, y=minCoord.y;
|
|
assert( agiPix(pic,x,y) == color );
|
|
*res += char( 0xF7 ); // Relative line command
|
|
*res += char( x ); // Starting x
|
|
*res += char( y ); // Starting y
|
|
agiPix(pic,x,y) = COLOR_NONE;
|
|
|
|
// Trace a line
|
|
while (true)
|
|
{
|
|
// Find out direction into which the line continues the longest
|
|
int max=0, maxdx=0xFF,maxdy=0xFF;
|
|
for ( int dx=-1; dx<=1; ++dx )
|
|
for ( int dy=-1; dy<=1; ++dy )
|
|
if ( dx != 0 || dy != 0 )
|
|
{
|
|
int len = traceToDir( pic, x,y, dx,dy, color, AGI_WIDTH );
|
|
if ( len > max )
|
|
{
|
|
max = len;
|
|
maxdx = dx;
|
|
maxdy = dy;
|
|
}
|
|
}
|
|
|
|
if ( max==0 ) // Nowhere to go, end the trace
|
|
{
|
|
// Just for aesthetic reasons, try to start the next
|
|
// line nearby the end of the last one
|
|
minCount = findBestLineStartFromArea( pic, x-5,y-5, 10,10, color, singlePoints, minCoord );
|
|
if ( minCount == 0xFF )
|
|
break; // Not found, do a full pic scan
|
|
else
|
|
goto newTrace; // New line in the neighborhood, start from there
|
|
}
|
|
|
|
if ( max>6 ) // Clamp to maximum length of relative line
|
|
max=6;
|
|
|
|
int stepx = maxdx*max;
|
|
int stepy = maxdy*max;
|
|
|
|
assert(abs(stepx) <= 7);
|
|
assert(abs(stepy) <= 7);
|
|
|
|
// Write out the relative line stepping byte
|
|
unsigned char chr =
|
|
(((stepx<0)?1:0) << 7) | // x sign
|
|
(abs(stepx) << 4) | // Xdisp
|
|
(((stepy<0)?1:0) << 3) | // y sign
|
|
(abs(stepy) << 0); // Ydisp
|
|
|
|
assert((chr&0xF0) != 0xF0); // Must not be mistaken for a draw command
|
|
|
|
*res += char(chr);
|
|
|
|
// Clear the line
|
|
while (max>=0)
|
|
{
|
|
agiPix(pic,x+maxdx*max,y+maxdy*max) = COLOR_NONE;
|
|
--max;
|
|
}
|
|
|
|
// Move pointer to the new end
|
|
x += stepx;
|
|
y += stepy;
|
|
}
|
|
}
|
|
// When all continuous lines are drawn,
|
|
// write out single points as a brush/plot/pen operation
|
|
if ( singlePoints.size()>0 )
|
|
{
|
|
*res += char( 0xFA ); // Pen command
|
|
for ( int i=0; i<(int)singlePoints.size(); ++i )
|
|
{
|
|
assert( (unsigned char)singlePoints[i].x < 0xF0);
|
|
assert( (unsigned char)singlePoints[i].y < 0xF0);
|
|
*res += char( singlePoints[i].x );
|
|
*res += char( singlePoints[i].y );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert either a visual or a priority image and write to res
|
|
void oneChannelToAGIPicture( const QImage& chan, QByteArray* res, bool isPri )
|
|
{
|
|
unsigned char pic[AGI_WIDTH][AGI_HEIGHT];
|
|
QuantizeAGI( chan, pic );
|
|
|
|
|
|
// We're doing one channel at a time, disable the other one first
|
|
*res += char( isPri ? 0xF1 : 0xF3 );
|
|
|
|
// Clear the default color, no sense in filling/drawing with it:
|
|
unsigned default_color = isPri ? 4 : 15; // 4=red for pri, 15=white for visual
|
|
for (int y=0; y<AGI_HEIGHT; ++y )
|
|
for (int x=0; x<AGI_WIDTH; ++x )
|
|
if ( agiPix(pic,x,y) == default_color )
|
|
agiPix(pic,x,y) = COLOR_NONE;
|
|
|
|
// Record flood fill areas and empty them
|
|
// (color by color in reverse order to favor black outlines)
|
|
std::vector<CoordColor> floodFills;
|
|
for ( int c=N_COLORS-1; c>=0; --c )
|
|
for (int y=0; y<AGI_HEIGHT; ++y )
|
|
for (int x=0; x<AGI_WIDTH; ++x )
|
|
if ( agiPix(pic,x,y) == c && isOnFloodFillArea(pic,x,y) )
|
|
{
|
|
floodFills.push_back(CoordColor(x,y, agiPix(pic,x,y)));
|
|
floodFillEmpty(pic,x,y);
|
|
}
|
|
|
|
// Replace lines and single pixels with commands
|
|
replaceLines( pic, res, isPri );
|
|
|
|
// Write out flood fills
|
|
unsigned char curColor = COLOR_NONE;
|
|
for ( int i=0; i<(int)floodFills.size(); ++i)
|
|
{
|
|
CoordColor c = floodFills[i];
|
|
assert( c.c != default_color ); // we shouldn't need to fill with default color
|
|
floodFills.erase( floodFills.begin()+i);
|
|
i--;
|
|
if ( curColor != c.c )
|
|
{
|
|
curColor = c.c;
|
|
*res += char( isPri ? 0xF2 : 0xF0 ); // Change color command
|
|
*res += char( c.c ); // Color index
|
|
*res += char( 0xF8 ); // Flood fill command
|
|
}
|
|
assert( curColor != COLOR_NONE ); // i.e: flood fill command written
|
|
*res += char( c.x );
|
|
*res += char( c.y );
|
|
}
|
|
assert( floodFills.empty());
|
|
}
|
|
|
|
|
|
// Converts bitmaps (pic and pri) into an AGI "picture" resrouce.
|
|
// Returns NULL if success, or error message otherwise.
|
|
const char* bitmapToAGIPicture( const QImage& pic, const QImage& pri, QByteArray* res )
|
|
{
|
|
// Check given images & convert to EGA palette arrays
|
|
if ( (pic.width() != AGI_WIDTH*2 && pic.width() != AGI_WIDTH) || pic.height() < AGI_HEIGHT )
|
|
return("Picture bitmap size must be 160x168 or 320x168.");
|
|
if ( !pri.isNull())
|
|
if ( (pri.width() != AGI_WIDTH*2 && pri.width() != AGI_WIDTH) || pri.height() < AGI_HEIGHT )
|
|
return("Priority bitmap size must be 160x168 or 320x168.");
|
|
|
|
// Set brush once at the beginning
|
|
*res += char( 0xF9 ); // Change pen size & style
|
|
*res += char( 0x00 ); // solid single pixel
|
|
|
|
oneChannelToAGIPicture( pic, res, false);
|
|
if ( !pri.isNull())
|
|
oneChannelToAGIPicture( pri, res, true);
|
|
|
|
*res += char( 0xFF ); // eof marker
|
|
|
|
return NULL;
|
|
}
|