diff --git a/FileConv/Gfx/GSFinderIcon.cs b/FileConv/Gfx/GSFinderIcon.cs index 07aed03..d96f70f 100644 --- a/FileConv/Gfx/GSFinderIcon.cs +++ b/FileConv/Gfx/GSFinderIcon.cs @@ -287,8 +287,9 @@ private static IConvOutput RenderIconGrid(IconFile iconFile, Notes loadNotes) { // One row per icon entry, 4 columns each. ImageGrid.Builder bob = new ImageGrid.Builder(); bob.SetGeometry(maxIconWidth, maxIconHeight, NUM_COLS); + bob.SetChunkGeometry(20, 16); bob.SetRange(0, iconFile.IconRecords.Count * NUM_COLS); - bob.SetColors(ICON_PALETTE, COLOR_FRAME, COLOR_BKGND, COLOR_FRAME, COLOR_FRAME); + bob.SetColors(ICON_PALETTE, COLOR_FRAME, COLOR_BKGND, COLOR_FRAME, COLOR_BKGND); bob.SetLabels(hasLeftLabels: true, hasTopLabels: false, leftLabelIsRow: true, 10); ImageGrid grid; try { diff --git a/FileConv/Gfx/ImageGrid.cs b/FileConv/Gfx/ImageGrid.cs index c7d4f7d..65954a4 100644 --- a/FileConv/Gfx/ImageGrid.cs +++ b/FileConv/Gfx/ImageGrid.cs @@ -27,6 +27,9 @@ namespace FileConv.Gfx { /// lines. It's valid for a grid to hold, say, indices $38-$89, in which case it will /// provide rows $30 through $80, spanning $30-$8f. The padding cells are drawn in a /// different color. + /// Some files, like Finder icons, can be very large in one dimension. It makes sense + /// to render these as multiple independent grids on the same bitmap, each with their own + /// set of side labels. These are called "chunks". /// Rendering within a cell is bounds-checked, so we don't throw exceptions even /// if we get crazy values. Instead, errors are noted in the log. This allows us to /// provide best-effort output for partially broken inputs. @@ -39,6 +42,8 @@ internal class ImageGrid { private int mCellWidth; // width of a cell private int mCellHeight; // height of a cell private int mMaxPerRow; // max number of items in a row + private int mMaxRowsPerChunk; // max number of rows in a chunk + private int mHorizChunkSep; // horizontal pixel distance between chunks private byte mLabelFg; // foreground color for labels private byte mLabelBg; // background color for labels private byte mBorderColor; // grid border line color @@ -55,11 +60,25 @@ internal class ImageGrid { public int CellWidth => mCellWidth; public int CellHeight => mCellHeight; - private int mStartIndex; // index of item in top-left cell (may be padding) - private int mEndIndex; // index of item past bottom-right cell (may be pad) - private int mNumRows; // number of grid rows - private int mGridLeft; // horizontal pixel offset of leftmost grid line - private int mGridTop; // vertical pixel offset of top grid line + /// + /// One chunk of the grid. + /// + /// + /// Most things only need one chunk. For Finder icons, which are very tall and narrow, + /// we want to split large files into multiple grids to avoid blowing out the 4096x4096 + /// limit on the bitmap. + /// + private class Chunk { + public int mStartIndex; // index of item in top-left cell (may be padding) + public int mEndIndex; // index of item past bottom-right cell (may be pad) + public int mNumRows; // number of grid rows + public int mLeft; // horizontal pixel offset of left edge of chunk + public int mTop; // vertical pixel offet of top edge of chunk + public int mGridLeft; // horizontal pixel offset of leftmost grid line + public int mGridTop; // vertical pixel offset of top grid line + } + + private Chunk[] mChunks; // Constants. These can become variables. private const int LABEL_CHAR_WIDTH = 8; // 8x8 font width @@ -67,6 +86,8 @@ internal class ImageGrid { private const int GRID_THICKNESS = 1; // note: values != 1 are not implemented private const int EDGE_PAD = 1; // extra padding before top/left labels + #region Builder + /// /// Builder class, so we don't have to call a constructor with tons of arguments. /// @@ -76,7 +97,7 @@ public Builder() { } public ImageGrid Create() { Debug.Assert(FirstIndex >= 0 && CellWidth >= 0 && LabelFgColor != 255); return new ImageGrid(FirstIndex, NumItems, - CellWidth, CellHeight, MaxPerRow, + CellWidth, CellHeight, MaxPerRow, MaxRowsPerChunk, HorizChunkSep, Palette, LabelFgColor, LabelBgColor, BorderColor, PadCellColor, HasLeftLabels, HasTopLabels, LeftLabelIsRow, LabelRadix); } @@ -86,6 +107,8 @@ public ImageGrid Create() { public int CellWidth { get; private set; } = -1; public int CellHeight { get; private set; } public int MaxPerRow { get; private set; } + public int MaxRowsPerChunk { get; private set; } = int.MaxValue; + public int HorizChunkSep { get; private set; } public Palette8 Palette { get; private set; } = Palette8.EMPTY_PALETTE; public byte LabelFgColor { get; private set; } = 255; public byte LabelBgColor { get; private set; } @@ -111,6 +134,19 @@ public void SetGeometry(int cellWidth, int cellHeight, int maxPerRow) { MaxPerRow = maxPerRow; } + /// + /// Sets the chunk geometry. Optional. + /// + /// Maximum number of rows we display in each + /// chunk. + /// Horizontal pixel distance between chunks. + public void SetChunkGeometry(int maxRowsPerChunk, int horizSep) { + Debug.Assert(maxRowsPerChunk > 0); + Debug.Assert(horizSep > 0); + MaxRowsPerChunk = maxRowsPerChunk; + HorizChunkSep = horizSep; + } + /// /// Sets the item range entries. Mandatory. /// @@ -163,11 +199,14 @@ public void SetLabels(bool hasLeftLabels, bool hasTopLabels, bool leftLabelIsRow } } + #endregion Builder + /// /// Internal constructor. /// private ImageGrid(int firstIndex, int numItems, int cellWidth, int cellHeight, - int maxPerRow, Palette8 palette, byte labelFgColor, byte labelBgColor, + int maxPerRow, int maxRowsPerChunk, int horizChunkSep, + Palette8 palette, byte labelFgColor, byte labelBgColor, byte borderColor, byte padCellColor, bool hasLeftLabels, bool hasTopLabels, bool leftLabelIsRow, int labelRadix) { // If the items are smaller than the frame labels, increase the size. @@ -183,6 +222,8 @@ private ImageGrid(int firstIndex, int numItems, int cellWidth, int cellHeight, mCellWidth = cellWidth; mCellHeight = cellHeight; mMaxPerRow = maxPerRow; + mMaxRowsPerChunk = maxRowsPerChunk; + mHorizChunkSep = horizChunkSep; mLabelFg = labelFgColor; mLabelBg = labelBgColor; mBorderColor = borderColor; @@ -192,45 +233,94 @@ private ImageGrid(int firstIndex, int numItems, int cellWidth, int cellHeight, mLeftLabelIsRow = leftLabelIsRow; mLabelRadix = labelRadix; - mStartIndex = firstIndex - (firstIndex % mMaxPerRow); + // First index, padded to fill row. + int startIndex = firstIndex - (firstIndex % mMaxPerRow); + // Last index (inclusive), not padded. int lastIndex = firstIndex + numItems - 1; - mEndIndex = (lastIndex + mMaxPerRow) - (lastIndex % mMaxPerRow); - mNumRows = (mEndIndex - mStartIndex) / mMaxPerRow; + // Last index (exclusive), padded to fill row. + int endIndex = (lastIndex + mMaxPerRow) - (lastIndex % mMaxPerRow); + // Total number of rows needed. + int totalNumRows = (endIndex - startIndex) / mMaxPerRow; + // Number of chunks. + if (mMaxRowsPerChunk > totalNumRows) { + mMaxRowsPerChunk = totalNumRows; + } + int numChunks = 1 + (totalNumRows - 1) / mMaxRowsPerChunk; + Debug.Assert(numChunks > 0); // Width must include the left-side label, plus one space for padding, and a full // row of cells separated with grid lines. int numDigits; string labelFmt = (mLabelRadix == 16) ? "x" : ""; // log10 or log16 of max num if (leftLabelIsRow) { - numDigits = mNumRows.ToString(labelFmt).Length; + numDigits = totalNumRows.ToString(labelFmt).Length; } else { numDigits = lastIndex.ToString(labelFmt).Length; } - mGridLeft = 0; - int width = (cellWidth * mMaxPerRow) + (GRID_THICKNESS * (mMaxPerRow + 1)); - if (hasLeftLabels) { - width += EDGE_PAD + (numDigits * LABEL_CHAR_WIDTH); - mGridLeft = EDGE_PAD + (numDigits * LABEL_CHAR_WIDTH); - } + int totalWidth = 0; + int totalHeight = 0; + int leftOffset = 0; + int topOffset = 0; + + // Create chunks. + mChunks = new Chunk[numChunks]; + for (int chn = 0; chn < numChunks; chn++) { + Chunk chunk = mChunks[chn] = new Chunk(); + if (chn != 0) { + leftOffset += mHorizChunkSep; + } - mGridTop = 0; - int height = (mNumRows * cellHeight) + (GRID_THICKNESS * (mNumRows + 1)); - if (hasTopLabels) { - height += EDGE_PAD + LABEL_CHAR_HEIGHT; - mGridTop = EDGE_PAD + LABEL_CHAR_HEIGHT; - } + chunk.mStartIndex = startIndex; + chunk.mEndIndex = startIndex + mMaxRowsPerChunk * mMaxPerRow; + chunk.mNumRows = (chunk.mEndIndex - chunk.mStartIndex) / mMaxPerRow; + // Last chunk may have a partial set of rows. + if (mMaxRowsPerChunk * (chn + 1) > totalNumRows) { + Debug.Assert(chn > 0); + chunk.mNumRows = totalNumRows - mMaxRowsPerChunk * chn; + // Adjust the end index so we don't draw padding cells in unused rows. + chunk.mEndIndex = startIndex + chunk.mNumRows * mMaxPerRow; + } + + chunk.mLeft = chunk.mGridLeft = leftOffset; + int width = (cellWidth * mMaxPerRow) + (GRID_THICKNESS * (mMaxPerRow + 1)); + if (hasLeftLabels) { + width += EDGE_PAD + (numDigits * LABEL_CHAR_WIDTH); + chunk.mGridLeft += EDGE_PAD + (numDigits * LABEL_CHAR_WIDTH); + } + + chunk.mTop = chunk.mGridTop = topOffset; + int height = (chunk.mNumRows * cellHeight) + (GRID_THICKNESS * (chunk.mNumRows + 1)); + if (hasTopLabels) { + height += EDGE_PAD + LABEL_CHAR_HEIGHT; + chunk.mGridTop += EDGE_PAD + LABEL_CHAR_HEIGHT; + } + + if (chn != 0) { + totalWidth += mHorizChunkSep; + } + totalWidth += width; + totalHeight = Math.Max(totalHeight, height); - if (width > Bitmap8.MAX_DIMENSION || height > Bitmap8.MAX_DIMENSION) { - throw new BadImageFormatException("bitmap would be " + width + "x" + height + - ", exceeding maximum allowed size"); + leftOffset += width; + + startIndex += chunk.mEndIndex - chunk.mStartIndex; } - Bitmap = new Bitmap8(width, height); + // Create bitmap. + if (totalWidth > Bitmap8.MAX_DIMENSION || totalHeight > Bitmap8.MAX_DIMENSION) { + throw new BadImageFormatException("bitmap would be " + totalWidth + "x" + + totalHeight + ", exceeding maximum allowed size"); + } + Bitmap = new Bitmap8(totalWidth, totalHeight); Bitmap.SetPalette(palette); - DrawLabels(numDigits, mEndIndex); - DrawGrid(); - DrawPadFiller(); + + // Draw labels, grids, and filler for each chunk. + foreach (Chunk chunk in mChunks) { + DrawLabels(chunk, numDigits); + DrawGrid(chunk); + DrawPadFiller(chunk); + } } private static readonly char[] sHexDigit = { @@ -240,34 +330,35 @@ private ImageGrid(int firstIndex, int numItems, int cellWidth, int cellHeight, /// /// Draws the hex labels across the top and down the left edge. /// - private void DrawLabels(int numDigits, int endIndex) { - Debug.Assert(mStartIndex % mMaxPerRow == 0); - Debug.Assert(endIndex % mMaxPerRow == 0); + private void DrawLabels(Chunk chunk, int numDigits) { + Debug.Assert(chunk.mStartIndex % mMaxPerRow == 0); + Debug.Assert(chunk.mEndIndex % mMaxPerRow == 0); string labelFmt = (mLabelRadix == 16) ? "X" : ""; + int labelBase = chunk.mStartIndex / mMaxPerRow; + if (mHasLeftLabels) { // Draw the labels down the left edge. - int startRow = 0; + int startLine = chunk.mTop; if (mHasTopLabels) { - startRow += EDGE_PAD + LABEL_CHAR_HEIGHT; + startLine += EDGE_PAD + LABEL_CHAR_HEIGHT; } - Bitmap.FillRect(0, startRow, EDGE_PAD + LABEL_CHAR_WIDTH * numDigits, - Bitmap.Height - startRow, mLabelBg); - startRow += GRID_THICKNESS; + Bitmap.FillRect(chunk.mLeft, startLine, EDGE_PAD + LABEL_CHAR_WIDTH * numDigits, + chunk.mNumRows * (mCellHeight + GRID_THICKNESS) + 1, mLabelBg); + startLine += GRID_THICKNESS; - startRow += (mCellHeight - LABEL_CHAR_HEIGHT) / 2; // center - int numRows = (endIndex - mStartIndex) / mMaxPerRow; - for (int row = 0; row < numRows; row++) { + startLine += (mCellHeight - LABEL_CHAR_HEIGHT) / 2; // center + for (int row = 0; row < chunk.mNumRows; row++) { // Generate the label string. string rowLabelStr; if (mLeftLabelIsRow) { - rowLabelStr = row.ToString(labelFmt); + rowLabelStr = (labelBase + row).ToString(labelFmt); } else { - rowLabelStr = (mStartIndex + row * mMaxPerRow).ToString(labelFmt); + rowLabelStr = (chunk.mStartIndex + row * mMaxPerRow).ToString(labelFmt); } - int ypos = startRow + (mCellHeight + GRID_THICKNESS) * row; - int xpos = EDGE_PAD; + int ypos = startLine + (mCellHeight + GRID_THICKNESS) * row; + int xpos = chunk.mLeft + EDGE_PAD; foreach (char ch in rowLabelStr) { Bitmap.DrawChar(ch, xpos, ypos, mLabelFg, mLabelBg); xpos += LABEL_CHAR_WIDTH; @@ -279,11 +370,11 @@ private void DrawLabels(int numDigits, int endIndex) { // Draw the labels across the top edge. // NOTE: this only works for ITEMS_PER_ROW <= 16. To handle more we'd want to // stack the label vertically, so we don't force the cells to be wider. - int startCol = 0; + int startCol = chunk.mLeft; if (mHasLeftLabels) { startCol += EDGE_PAD + (numDigits * LABEL_CHAR_WIDTH); } - Bitmap.FillRect(startCol, 0, Bitmap.Width - startCol, + Bitmap.FillRect(startCol, chunk.mTop, Bitmap.Width - startCol, EDGE_PAD + LABEL_CHAR_HEIGHT, mLabelBg); startCol += GRID_THICKNESS; @@ -296,13 +387,14 @@ private void DrawLabels(int numDigits, int endIndex) { labelCh = '*'; } Bitmap.DrawChar(labelCh, startCol + col * (mCellWidth + GRID_THICKNESS), - EDGE_PAD, mLabelFg, mLabelBg); + chunk.mTop + EDGE_PAD, mLabelFg, mLabelBg); } } if (mHasLeftLabels && mHasTopLabels) { // Fill in the top-left corner, if we have both vertical and horizontal labels. - Bitmap.FillRect(0, 0, EDGE_PAD + (numDigits * LABEL_CHAR_WIDTH) + GRID_THICKNESS, + Bitmap.FillRect(chunk.mLeft, chunk.mTop, + EDGE_PAD + (numDigits * LABEL_CHAR_WIDTH) + GRID_THICKNESS, EDGE_PAD + LABEL_CHAR_HEIGHT + GRID_THICKNESS, mLabelBg); } } @@ -310,34 +402,33 @@ private void DrawLabels(int numDigits, int endIndex) { /// /// Draws a grid around the cells. /// - private void DrawGrid() { - int xc = mGridLeft; - int yc = mGridTop; - + private void DrawGrid(Chunk chunk) { for (int col = 0; col <= mMaxPerRow; col++) { - DrawVerticalGridLine(xc + col * (mCellWidth + GRID_THICKNESS)); + DrawVerticalGridLine(chunk, chunk.mGridLeft + col * (mCellWidth + GRID_THICKNESS)); } - for (int row = 0; row <= mNumRows; row++) { - DrawHorizontalGridLine(yc + row * (mCellHeight + GRID_THICKNESS)); + for (int row = 0; row <= chunk.mNumRows; row++) { + DrawHorizontalGridLine(chunk,chunk.mGridTop + row * (mCellHeight + GRID_THICKNESS)); } // Set the bottom-right pixel. - Bitmap.SetPixelIndex(Bitmap.Width - 1, Bitmap.Height - 1, mBorderColor); + int xc = chunk.mGridLeft + mMaxPerRow * (mCellWidth + GRID_THICKNESS); + int yc = chunk.mGridTop + chunk.mNumRows * (mCellHeight + GRID_THICKNESS); + Bitmap.SetPixelIndex(xc, yc, mBorderColor); } - private void DrawVerticalGridLine(int xc) { + private void DrawVerticalGridLine(Chunk chunk, int xc) { Debug.Assert(GRID_THICKNESS == 1); - int gridHeight = (mCellHeight + GRID_THICKNESS) * mNumRows; - for (int yc = mGridTop; yc < mGridTop + gridHeight; yc++) { + int gridHeight = (mCellHeight + GRID_THICKNESS) * chunk.mNumRows; + for (int yc = chunk.mGridTop; yc < chunk.mGridTop + gridHeight; yc++) { // TODO: handle non-1 values of GRID_THICKNESS Bitmap.SetPixelIndex(xc, yc, mBorderColor); } } - private void DrawHorizontalGridLine(int yc) { + private void DrawHorizontalGridLine(Chunk chunk,int yc) { Debug.Assert(GRID_THICKNESS == 1); int gridWidth = (mCellWidth + GRID_THICKNESS) * mMaxPerRow; - for (int xc = mGridLeft; xc < mGridLeft + gridWidth; xc++) { + for (int xc = chunk.mGridLeft; xc < chunk.mGridLeft + gridWidth; xc++) { // TODO: handle non-1 values of GRID_THICKNESS Bitmap.SetPixelIndex(xc, yc, mBorderColor); } @@ -346,11 +437,11 @@ private void DrawHorizontalGridLine(int yc) { /// /// Fills in the cells at the start and end that are just there for padding. /// - private void DrawPadFiller() { - for (int i = mStartIndex; i < mFirstIndex; i++) { + private void DrawPadFiller(Chunk chunk) { + for (int i = chunk.mStartIndex; i < mFirstIndex; i++) { DoDrawRect(i, 0, 0, mCellWidth, mCellHeight, mPadCellColor); } - for (int i = mFirstIndex + mNumItems; i < mEndIndex; i++) { + for (int i = mFirstIndex + mNumItems; i < chunk.mEndIndex; i++) { DoDrawRect(i, 0, 0, mCellWidth, mCellHeight, mPadCellColor); } } @@ -359,10 +450,13 @@ private void DrawPadFiller() { /// Calculates the pixel position of the specified cell. /// private void CalcCellPosn(int cellIndex, out int cellLeft, out int cellTop) { - int cellCol = (cellIndex - mStartIndex) % mMaxPerRow; - int cellRow = (cellIndex - mStartIndex) / mMaxPerRow; - cellLeft = mGridLeft + GRID_THICKNESS + (mCellWidth + GRID_THICKNESS) * cellCol; - cellTop = mGridTop + GRID_THICKNESS + (mCellHeight + GRID_THICKNESS) * cellRow; + int chunkNum = (cellIndex / mMaxPerRow) / mMaxRowsPerChunk; + Debug.Assert(chunkNum < mChunks.Length); + Chunk chunk = mChunks[chunkNum]; + int cellCol = (cellIndex - chunk.mStartIndex) % mMaxPerRow; + int cellRow = (cellIndex - chunk.mStartIndex) / mMaxPerRow; + cellLeft = chunk.mGridLeft + GRID_THICKNESS + (mCellWidth + GRID_THICKNESS) * cellCol; + cellTop = chunk.mGridTop + GRID_THICKNESS + (mCellHeight + GRID_THICKNESS) * cellRow; } /// diff --git a/FileConv/Gfx/PrintShopFont.cs b/FileConv/Gfx/PrintShopFont.cs index 454b95e..63390b2 100644 --- a/FileConv/Gfx/PrintShopFont.cs +++ b/FileConv/Gfx/PrintShopFont.cs @@ -356,7 +356,7 @@ private bool DataPointersAreValid() if (!(addr >= 0x6000 && addr < FileAttrs.AuxType + DataBuf.Length)) { - Debug.WriteLine("Address out of range for Character " + idx); + Debug.WriteLine("PSF: address out of range for character " + idx); } valid &= addr >= 0x6000 && addr < FileAttrs.AuxType + DataBuf.Length; @@ -365,7 +365,7 @@ private bool DataPointersAreValid() if (!valid) { - Debug.WriteLine("Valid is not set for this font."); + Debug.WriteLine("PSF: valid is not set for this font"); } return valid;