/* * AppleCommander - An Apple ][ image utility. * Copyright (C) 2002-2022 by Robert Greene * robgreene at users.sourceforge.net * * 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., * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.webcodepro.applecommander.storage.filters; import java.io.ByteArrayOutputStream; import java.io.PrintWriter; import java.text.NumberFormat; import com.webcodepro.applecommander.storage.FileEntry; import com.webcodepro.applecommander.storage.FileFilter; import com.webcodepro.applecommander.util.AppleUtil; /** * Export an AppleWorks SpreadSheet file. * The spread-sheet file starts with a 300 byte header, followed by * multiple cells. *

* See: http://www.gno.org/pub/apple2/doc/apple/filetypes/ftn.1b.xxxx *

* Date Created: Feb 23, 2003 * @author Rob Greene */ public class AppleWorksSpreadSheetFileFilter implements FileFilter { /** * SSMinVers. The minimum version of AppleWorks needed * to read this file. If this file contains version 3.0- * specific functions (such as calculated labels or new * functions), this byte will contain the version number * 30 ($1E). Otherwise it will be zero ($00). */ private static final int HEADER_SSMINVERS_BYTE = 242; /** * Value Constant mask bytes +000 and +001. */ private static final int CELL_VALUE_CONSTANT_MASK = 0xa0e0; /** * Value Constant id byte +000 and +001. */ private static final int CELL_VALUE_CONSTANT_ID = 0xa000; /** * Value Label mask bytes +000 and +001. */ private static final int CELL_VALUE_LABEL_MASK = 0xa088; /** * Value Label id bytes +000 and +001. */ private static final int CELL_VALUE_LABEL_ID = 0x8088; /** * Value Formula mask bytes +000 and +001. */ private static final int CELL_VALUE_FORMULA_MASK = 0xa080; /** * Value Formula id bytes +000 and +001. */ private static final int CELL_VALUE_FORMULA_ID = 0x8080; /** * Propagated Value Cells mask byte +000. */ private static final int CELL_PROPAGATED_VALUE_MASK = 0xa0; /** * Propagated Value Cells id byte +000. */ private static final int CELL_PROPAGATED_VALUE_ID = 0x20; /** * "Regular" Label Cell mask byte +000. */ private static final int CELL_LABEL_VALUE_MASK = 0xe0; /** * "Regular" Label Cell id byte +000. */ private static final int CELL_LABEL_VALUE_ID = 0x00; /** * This is the value that the formulas begin at. */ private static final int FORMULA_OFFSET = 0xc0; /** * These are the formulas starting at FORMULA_OFFSET. */ private static final String[] formulaText = { "@Deg", "@Rad", "@Pi", "@True", "@False", "@Not", "@IsBlank", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ "@IsNA", "@IsError", "@Exp", "@Ln", "@Log", "@Cos", "@Sin", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ "@Tan", "@ACos", "@ASin", "@ATan2", "@ATan", "@Mod", "@FV", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ "@PV", "@PMT", "@Term", "@Rate", "@Round", "@Or", "@And", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ "@Sum", "@Avg", "@Choose", "@Count", "@Error", "@IRR", "@If", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ "@Int", "@Lookup", "@Max", "@Min", "@NA", "@NPV", "@Sqrt", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ "@Abs", null, "<>", ">=", "<=", "=", ">", "<", ",", "^", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ //$NON-NLS-9$ ")", "-", "+", "/", "*", "(", "-", "+", "..", null, null, //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ //$NON-NLS-9$ null }; /** * The @Error formula requires 3 trailing zero bytes. */ private static final int FORMULA_ERROR_CODE = 0xe0; /** * The @NA formula requires 3 trailing zero bytes. */ private static final int FORMULA_NA_CODE = 0xe7; /** * The $FD formula control byte indicates the next 8 bytes * are a SANE number. */ private static final int FORMULA_SANE_CODE = 0xfd; /** * The $FE formula control byte indicates that the next * 3 bytes indicate row, column reference. */ private static final int FORMULA_ROW_COLUMN_CODE = 0xfe; /** * The $FF formula control byte indicates that a Pascal * string follows. The next byte is the length of the string. */ private static final int FORMULA_STRING_CODE = 0xff; /** * Process the given FileEntry and return a byte array with filtered data. * @see com.webcodepro.applecommander.storage.FileFilter#filter(FileEntry) */ public byte[] filter(FileEntry fileEntry) { byte[] fileData = fileEntry.getFileData(); ByteArrayOutputStream byteArray = new ByteArrayOutputStream(); PrintWriter printWriter = new PrintWriter(byteArray, true); boolean isVersion30 = (fileData[HEADER_SSMINVERS_BYTE] == 30); int offset = 300 + (isVersion30 ? 2 : 0); int rowLength = AppleUtil.getWordValue(fileData, offset); while (rowLength != 0xffff) { int rowNumber = AppleUtil.getWordValue(fileData, offset+2); offset+= 4; processRow(printWriter, fileData, offset, rowNumber); offset+= rowLength - 2; rowLength = AppleUtil.getWordValue(fileData, offset); } // return CSV file: return byteArray.toByteArray(); } /** * Give suggested file name. * @see com.webcodepro.applecommander.storage.FileFilter#getSuggestedFileName(FileEntry) */ public String getSuggestedFileName(FileEntry fileEntry) { return fileEntry.getFilename() + ".csv"; //$NON-NLS-1$ } /** * Process an entire row. */ public void processRow(PrintWriter printWriter, byte[] fileData, int offset, int rowNumber) { int column = 0; while (true) { int rowControl = AppleUtil.getUnsignedByte(fileData[offset]); if (rowControl <= 0x7f) { // process row if (column > 0) printWriter.print(","); //$NON-NLS-1$ processCell(printWriter, fileData, offset+1, rowControl, rowNumber, column); offset+= rowControl; } else if (rowControl < 0xff) { // skip rows if (column > 0) printWriter.print(","); //$NON-NLS-1$ int columns = rowControl - 0x80; skipColumns(column, printWriter, columns); column+= columns; } else { // end of row ($FF) printWriter.println(); break; } offset++; column++; } } /** * Skip the given number of columns. */ protected void skipColumns(int column, PrintWriter printWriter, int columns) { while (columns > 0) { if (column > 0) printWriter.print(","); //$NON-NLS-1$ printWriter.print("\",\""); //$NON-NLS-1$ columns--; column++; } } /** * Process an individual cell. */ protected void processCell(PrintWriter printWriter, byte[] fileData, int offset, int length, int currentRow, int currentColumn) { int byte0 = AppleUtil.getUnsignedByte(fileData[offset]); int byte1 = AppleUtil.getUnsignedByte(fileData[offset+1]); int cellFlag = (byte0 << 8) + byte1; if ((cellFlag & CELL_VALUE_CONSTANT_MASK) == CELL_VALUE_CONSTANT_ID) { double value = AppleUtil.getSaneNumber(fileData, offset+2); printWriter.print(value); } else if ((cellFlag & CELL_VALUE_LABEL_MASK) == CELL_VALUE_LABEL_ID) { // This is AW 3.0 or later, skipping until an example is found. } else if ((cellFlag & CELL_VALUE_FORMULA_MASK) == CELL_VALUE_FORMULA_ID) { int i = 10; printWriter.print('"'); while (i < length) { int controlByte = AppleUtil.getUnsignedByte(fileData[offset+i]); i++; switch (controlByte) { case FORMULA_ERROR_CODE: i+= 3; // skip 3 zero bytes break; case FORMULA_NA_CODE: i+= 3; // skip 3 zero bytes break; case FORMULA_SANE_CODE: double value = AppleUtil.getSaneNumber(fileData, offset+i); printWriter.print(value); i+= 8; // skip past SANE number break; case FORMULA_ROW_COLUMN_CODE: int column = fileData[offset+i]; int row = AppleUtil.getSignedWordValue(fileData, offset+i+1); printWriter.print(getColumnReference(currentColumn + column)); printWriter.print(getRowReference(currentRow + row)); i+= 3; // skip past row/column reference break; case FORMULA_STRING_CODE: String string = AppleUtil.getPascalString(fileData, offset+i); printWriter.print(string); i+= string.length() + 1; // skip past string break; default: printWriter.print(formulaText[controlByte - FORMULA_OFFSET]); break; } } printWriter.print('"'); } else if ((byte0 & CELL_PROPAGATED_VALUE_MASK) == CELL_PROPAGATED_VALUE_ID) { char ch = (char) AppleUtil.getUnsignedByte(fileData[offset+1]); printWriter.print('"'); for (int i=0; i<8; i++) { // 8 is an arbitrary cell width printWriter.print(ch); } printWriter.print('"'); } else if ((byte0 & CELL_LABEL_VALUE_MASK) == CELL_LABEL_VALUE_ID) { String string = AppleUtil.getString(fileData, offset+1, length-1); printWriter.print('"'); printWriter.print(string); printWriter.print('"'); } else { printWriter.print("\"Unknown Cell Contents!\""); //$NON-NLS-1$ } } /** * Build a column reference (convert to A or whatever it should be). */ protected String getColumnReference(int column) { int pos1 = column / 26; int pos2 = column % 26; StringBuffer buf = new StringBuffer(); if (pos1 > 0) { buf.append((char)('@' + pos1)); } buf.append((char)('@' + pos2)); return buf.toString(); } /** * Build a row reference. */ protected String getRowReference(int row) { NumberFormat formatter = NumberFormat.getInstance(); return formatter.format(row); } }