AppleCommander/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/filters/AppleWorksSpreadSheetFileFi...

280 lines
10 KiB
Java

/*
* 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.
* <p>
* See: http://www.gno.org/pub/apple2/doc/apple/filetypes/ftn.1b.xxxx
* <p>
* 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);
}
}