/* * Copyright (C) 2010-2011 JPEXS * * 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.jpexs.asdec; import com.jpexs.asdec.action.Action; import com.jpexs.asdec.tags.Tag; import com.jpexs.asdec.types.*; import com.jpexs.asdec.types.filters.*; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.List; /** * Class for writing data into SWF file * * @author JPEXS */ public class SWFOutputStream extends OutputStream { private OutputStream os; private int version; private long pos = 0; /** * Constructor * * @param os OutputStream for writing data * @param version Version of SWF */ public SWFOutputStream(OutputStream os, int version) { this.version = version; this.os = os; } /** * Writes byte to the stream * * @param b byte to write * @throws IOException */ @Override public void write(int b) throws IOException { alignByte(); os.write(b); pos++; } private void alignByte() throws IOException { if (bitPos > 0) { bitPos = 0; write(tempByte); tempByte = 0; } } /** * Writes UI8 (Unsigned 8bit integer) value to the stream * * @param val UI8 value to write * @throws IOException */ public void writeUI8(int val) throws IOException { write(val); } /** * Writes String value to the stream * * @param value String value * @throws IOException */ public void writeString(String value) throws IOException { write(value.getBytes("utf8")); write(0); } /** * Writes UI32 (Unsigned 32bit integer) value to the stream * * @param value UI32 value * @throws IOException */ public void writeUI32(long value) throws IOException { write((int) (value & 0xff)); write((int) ((value >> 8) & 0xff)); write((int) ((value >> 16) & 0xff)); write((int) ((value >> 24) & 0xff)); } /** * Writes UI16 (Unsigned 16bit integer) value to the stream * * @param value UI16 value * @throws IOException */ public void writeUI16(int value) throws IOException { write((int) (value & 0xff)); write((int) ((value >> 8) & 0xff)); } /** * Writes SI32 (Signed 32bit integer) value to the stream * * @param value SI32 value * @throws IOException */ public void writeSI32(long value) throws IOException { writeUI32(value); } /** * Writes SI16 (Signed 16bit integer) value to the stream * * @param value SI16 value * @throws IOException */ public void writeSI16(int value) throws IOException { writeUI16(value); } /** * Writes SI8 (Signed 8bit integer) value to the stream * * @param value SI8 value * @throws IOException */ public void writeSI8(int value) throws IOException { writeUI8(value); } /** * Writes FIXED (Fixed point 16.16) value to the stream * * @param value FIXED value * @throws IOException */ public void writeFIXED(double value) throws IOException { long valueLong = (long) (value * (1 << 16)); int beforePoint = (int) valueLong >> 16; int afterPoint = (int) valueLong % (1 << 16); writeUI16(afterPoint); writeUI16(beforePoint); } /** * Writes FIXED8 (Fixed point 8.8) value to the stream * * @param value FIXED8 value * @throws IOException */ public void writeFIXED8(float value) throws IOException { int beforePoint = (int) getIntPart(value); int afterPoint = (int) getIntPart((value + (value < 0 ? beforePoint : -beforePoint)) * 256); writeUI8(afterPoint); writeUI8(beforePoint); } private void writeLong(long value) throws IOException { byte writeBuffer[] = new byte[8]; writeBuffer[3] = (byte) (value >>> 56); writeBuffer[2] = (byte) (value >>> 48); writeBuffer[1] = (byte) (value >>> 40); writeBuffer[0] = (byte) (value >>> 32); writeBuffer[7] = (byte) (value >>> 24); writeBuffer[6] = (byte) (value >>> 16); writeBuffer[5] = (byte) (value >>> 8); writeBuffer[4] = (byte) (value >>> 0); write(writeBuffer); } /** * Writes DOUBLE (double precision floating point value) value to the stream * * @param value DOUBLE value * @throws IOException */ public void writeDOUBLE(double value) throws IOException { writeLong(Double.doubleToLongBits(value)); } /** * Writes FLOAT (single precision floating point value) value to the stream * * @param value FLOAT value * @throws IOException */ public void writeFLOAT(float value) throws IOException { writeUI32(Float.floatToIntBits(value)); } /** * Writes FLOAT16 (16bit floating point value) value to the stream * * @param value FLOAT16 value * @throws IOException */ public void writeFLOAT16(float value) throws IOException { int bits = Float.floatToRawIntBits(value); int sign = bits >> 31; int exponent = (bits >> 22) & 0xff; int mantisa = bits & 0x3FFFFF; mantisa = mantisa >> 13; writeUI16((sign << 15) + (exponent << 10) + mantisa); } /** * Writes EncodedU32 (Encoded unsigned 32bit value) value to the stream * * @param value U32 value * @throws IOException */ public void writeEncodedU32(long value) throws IOException { boolean loop = true; value = value & 0xFFFFFFFF; do { int ret = (int) (value & 0x7F); if (value < 0x80) { loop = false; } if (value > 0x7F) { ret += 0x80; } write(ret); value = value >> 7; } while (loop); } private int bitPos = 0; private int tempByte = 0; /** * Flushes data to underlying stream * * @throws IOException */ @Override public void flush() throws IOException { if (bitPos > 0) { bitPos = 0; write(tempByte); tempByte = 0; } os.flush(); } /** * Closes the stream * * @throws IOException */ @Override public void close() throws IOException { flush(); os.close(); } /** * Writes UB[nBits] (Unsigned-bit value) value to the stream * * @param nBits Number of bits which represent value * @param value Unsigned value to write * @throws IOException */ public void writeUB(int nBits, long value) throws IOException { for (int bit = 0; bit < nBits; bit++) { int nb = (int) ((value >> (nBits - 1 - bit)) & 1); tempByte += nb * (1 << (7 - bitPos)); bitPos++; if (bitPos == 8) { bitPos = 0; write(tempByte); tempByte = 0; } } } /** * Writes SB[nBits] (Signed-bit value) value to the stream * * @param nBits Number of bits which represent value * @param value Signed value to write * @throws IOException */ public void writeSB(int nBits, long value) throws IOException { writeUB(nBits, value); } /** * Writes FB[nBits] (Signed fixed-point bit value) value to the stream * * @param nBits Number of bits which represent value * @param value Double value to write * @throws IOException */ public void writeFB(int nBits, double value) throws IOException { long longVal = (long) (value * (1 << 16)); writeSB(nBits, longVal); } /** * Writes RECT value to the stream * * @param value RECT value * @throws IOException */ public void writeRECT(RECT value) throws IOException { int nBits = 0; nBits = enlargeBitCountS(nBits, value.Xmin); nBits = enlargeBitCountS(nBits, value.Xmax); nBits = enlargeBitCountS(nBits, value.Ymin); nBits = enlargeBitCountS(nBits, value.Ymax); writeUB(5, nBits); writeSB(nBits, value.Xmin); writeSB(nBits, value.Xmax); writeSB(nBits, value.Ymin); writeSB(nBits, value.Ymax); } /** * Writes list of Tag values to the stream * * @param tags List of tag values * @throws IOException */ public void writeTags(List tags) throws IOException { for (Tag tag : tags) { writeTag(tag); } } /** * Writes Tag value to the stream * * @param tag Tag value * @throws IOException */ public void writeTag(Tag tag) throws IOException { byte data[] = tag.getData(version); int tagLength = data.length; int tagID = tag.getId(); int tagIDLength = (tagID << 6); if ((tagLength < 0x3f) && (!tag.forceWriteAsLong)) { tagIDLength += tagLength; writeUI16(tagIDLength); } else { tagIDLength += 0x3f; writeUI16(tagIDLength); writeSI32(tagLength); } write(data); } /** * Calculates number of bits needed for representing unsigned value * * @param v Unsigned value * @return Number of bits */ public static int getNeededBitsU(long v) { int n = 32; long m = 0x80000000; if (v == 0x00000000) n = 0; else while (!((v & m) > 0)) { n--; m >>= 1; } return n; /*if (value == 0) { return 1; } return Long.toBinaryString(value).length();*/ } /** * Calculates number of bits needed for representing signed value * * @param v Signed value * @return Number of bits */ public static int getNeededBitsS(long v) { int n = 33; long m = 0x80000000; if ((v & m) == m) { if (v == 0xffffffff) n = 1; else while ((v & m) == m) { n--; m >>= 1; } } else { if (v == 0x00000000) n = 1; else while ((v & m) == 0) { n--; m >>= 1; } } return n; /*if (value == 0) { return 1; } if (value == -1) { return 2; } if (value < 0) { String str = Long.toBinaryString(value); for (int i = 0; i < str.length(); i++) { if (str.charAt(i) == '0') { return str.length() - 1 - i + 2; } } return 1; } else { return Long.toBinaryString(value).length() + 1; } */ } private static long getIntPart(double value) { if (value < 0) return (long) Math.ceil(value); return (long) Math.floor(value); } private static double getFloatPart(double value) { if (value < 0) return value - getIntPart(value); return value + getIntPart(value); } /** * Calculates number of bits needed for representing fixed-point value * * @param value Fixed-point value * @return Number of bits */ public static int getNeededBitsF(double value) { if (value == -1) return 18; long val = (long) (value * (1 << 16)); return getNeededBitsS(val); } private int enlargeBitCountU(int currentBitCount, long value) { int neededNew = getNeededBitsU(value); if (neededNew > currentBitCount) return neededNew; return currentBitCount; } private int enlargeBitCountS(int currentBitCount, long value) { int neededNew = getNeededBitsS(value); if (neededNew > currentBitCount) return neededNew; return currentBitCount; } private int enlargeBitCountF(int currentBitCount, double value) { int neededNew = getNeededBitsF(value); if (neededNew > currentBitCount) return neededNew; return currentBitCount; } /** * Writes MATRIX value to the stream * * @param value MATRIX value * @throws IOException */ public void writeMatrix(MATRIX value) throws IOException { writeUB(1, value.hasScale ? 1 : 0); if (value.hasScale) { int nBits = 0; nBits = enlargeBitCountF(nBits, value.scaleX); nBits = enlargeBitCountF(nBits, value.scaleY); nBits = value.scaleNBits; //FFFUUU writeUB(5, nBits); writeFB(nBits, value.scaleX); writeFB(nBits, value.scaleY); } writeUB(1, value.hasRotate ? 1 : 0); if (value.hasRotate) { int nBits = 0; nBits = enlargeBitCountF(nBits, value.rotateSkew0); nBits = enlargeBitCountF(nBits, value.rotateSkew1); nBits = value.rotateNBits; //FFFUUU writeUB(5, nBits); writeFB(nBits, value.rotateSkew0); writeFB(nBits, value.rotateSkew1); } int NTranslateBits = 0; NTranslateBits = enlargeBitCountS(NTranslateBits, value.translateX); NTranslateBits = enlargeBitCountS(NTranslateBits, value.translateY); NTranslateBits = value.translateNBits; //FFFUUU writeUB(5, NTranslateBits); writeSB(NTranslateBits, value.translateX); writeSB(NTranslateBits, value.translateY); alignByte(); } /** * Writes CXFORMWITHALPHA value to the stream * * @param value CXFORMWITHALPHA value * @throws IOException */ public void writeCXFORMWITHALPHA(CXFORMWITHALPHA value) throws IOException { writeUB(1, value.hasAddTerms ? 1 : 0); writeUB(1, value.hasMultTerms ? 1 : 0); int Nbits = 1; if (value.hasMultTerms) { Nbits = enlargeBitCountS(Nbits, value.redMultTerm); Nbits = enlargeBitCountS(Nbits, value.greenMultTerm); Nbits = enlargeBitCountS(Nbits, value.blueMultTerm); Nbits = enlargeBitCountS(Nbits, value.alphaMultTerm); } if (value.hasAddTerms) { Nbits = enlargeBitCountS(Nbits, value.redAddTerm); Nbits = enlargeBitCountS(Nbits, value.greenAddTerm); Nbits = enlargeBitCountS(Nbits, value.blueAddTerm); Nbits = enlargeBitCountS(Nbits, value.alphaAddTerm); } writeUB(4, Nbits); if (value.hasMultTerms) { writeSB(Nbits, value.redMultTerm); writeSB(Nbits, value.greenMultTerm); writeSB(Nbits, value.blueMultTerm); writeSB(Nbits, value.alphaMultTerm); } if (value.hasAddTerms) { writeSB(Nbits, value.redAddTerm); writeSB(Nbits, value.greenAddTerm); writeSB(Nbits, value.blueAddTerm); writeSB(Nbits, value.alphaAddTerm); } alignByte(); } /** * Writes CLIPEVENTFLAGS value to the stream * * @param value CLIPEVENTFLAGS value * @throws IOException */ public void writeCLIPEVENTFLAGS(CLIPEVENTFLAGS value) throws IOException { writeUB(1, value.clipEventKeyUp ? 1 : 0); writeUB(1, value.clipEventKeyDown ? 1 : 0); writeUB(1, value.clipEventMouseUp ? 1 : 0); writeUB(1, value.clipEventMouseDown ? 1 : 0); writeUB(1, value.clipEventMouseMove ? 1 : 0); writeUB(1, value.clipEventUnload ? 1 : 0); writeUB(1, value.clipEventEnterFrame ? 1 : 0); writeUB(1, value.clipEventLoad ? 1 : 0); writeUB(1, value.clipEventDragOver ? 1 : 0); writeUB(1, value.clipEventRollOut ? 1 : 0); writeUB(1, value.clipEventRollOver ? 1 : 0); writeUB(1, value.clipEventReleaseOutside ? 1 : 0); writeUB(1, value.clipEventRelease ? 1 : 0); writeUB(1, value.clipEventPress ? 1 : 0); writeUB(1, value.clipEventInitialize ? 1 : 0); writeUB(1, value.clipEventData ? 1 : 0); if (version >= 6) { writeUB(5, 0); writeUB(1, value.clipEventConstruct ? 1 : 0); writeUB(1, value.clipEventKeyPress ? 1 : 0); writeUB(1, value.clipEventDragOut ? 1 : 0); writeUB(8, 0); } } /** * Writes CLIPACTIONRECORD value to the stream * * @param value CLIPACTIONRECORD value * @throws IOException */ public void writeCLIPACTIONRECORD(CLIPACTIONRECORD value) throws IOException { writeCLIPEVENTFLAGS(value.eventFlags); ByteArrayOutputStream baos = new ByteArrayOutputStream(); SWFOutputStream sos = new SWFOutputStream(baos, version); if (value.eventFlags.clipEventKeyPress) { sos.writeUI8(value.keyCode); } sos.write(value.actionBytes); //sos.write(Action.actionsToBytes(value.actions, true, version)); sos.close(); byte data[] = baos.toByteArray(); writeUI32(data.length); //actionRecordSize write(data); } /** * Writes CLIPACTIONS value to the stream * * @param value CLIPACTIONS value * @throws IOException */ public void writeCLIPACTIONS(CLIPACTIONS value) throws IOException { writeUI16(0);//reserved writeCLIPEVENTFLAGS(value.allEventFlags); for (CLIPACTIONRECORD car : value.clipActionRecords) { writeCLIPACTIONRECORD(car); } if (version <= 5) { writeUI16(0); } else { writeUI32(0); } } /** * Writes COLORMATRIXFILTER value to the stream * * @param value COLORMATRIXFILTER value * @throws IOException */ public void writeCOLORMATRIXFILTER(COLORMATRIXFILTER value) throws IOException { for (int i = 0; i < 20; i++) { writeFLOAT(value.matrix[i]); } } /** * Writes RGBA value to the stream * * @param value RGBA value * @throws IOException */ public void writeRGBA(RGBA value) throws IOException { writeUI8(value.red); writeUI8(value.green); writeUI8(value.blue); writeUI8(value.alpha); } /** * Writes CONVOLUTIONFILTER value to the stream * * @param value CONVOLUTIONFILTER value * @throws IOException */ public void writeCONVOLUTIONFILTER(CONVOLUTIONFILTER value) throws IOException { writeUI8(value.matrixX); writeUI8(value.matrixY); writeFLOAT(value.divisor); writeFLOAT(value.bias); for (int x = 0; x < value.matrixX; x++) { for (int y = 0; y < value.matrixY; y++) { writeFLOAT(value.matrix[x][y]); } } writeRGBA(value.defaultColor); writeUB(6, 0); //reserved writeUB(1, value.clamp ? 1 : 0); writeUB(1, value.preserveAlpha ? 1 : 0); } /** * Writes BLURFILTER value to the stream * * @param value BLURFILTER value * @throws IOException */ public void writeBLURFILTER(BLURFILTER value) throws IOException { writeFIXED(value.blurX); writeFIXED(value.blurY); writeUB(5, value.passes); writeUB(3, 0);//reserved } /** * Writes DROPSHADOWFILTER value to the stream * * @param value DROPSHADOWFILTER value * @throws IOException */ public void writeDROPSHADOWFILTER(DROPSHADOWFILTER value) throws IOException { writeRGBA(value.dropShadowColor); writeFIXED(value.blurX); writeFIXED(value.blurY); writeFIXED(value.angle); writeFIXED(value.distance); writeFIXED8(value.strength); writeUB(1, value.innerShadow ? 1 : 0); writeUB(1, value.knockout ? 1 : 0); writeUB(1, value.compositeSource ? 1 : 0); writeUB(5, value.passes); } /** * Writes GLOWFILTER value to the stream * * @param value GLOWFILTER value * @throws IOException */ public void writeGLOWFILTER(GLOWFILTER value) throws IOException { writeRGBA(value.glowColor); writeFIXED(value.blurX); writeFIXED(value.blurY); writeFIXED8(value.strength); writeUB(1, value.innerGlow ? 1 : 0); writeUB(1, value.knockout ? 1 : 0); writeUB(1, value.compositeSource ? 1 : 0); writeUB(5, value.passes); } /** * Writes BEVELFILTER value to the stream * * @param value BEVELFILTER value * @throws IOException */ public void writeBEVELFILTER(BEVELFILTER value) throws IOException { writeRGBA(value.shadowColor); writeRGBA(value.highlightColor); writeFIXED(value.blurX); writeFIXED(value.blurY); writeFIXED(value.angle); writeFIXED(value.distance); writeFIXED8(value.strength); writeUB(1, value.innerShadow ? 1 : 0); writeUB(1, value.knockout ? 1 : 0); writeUB(1, value.compositeSource ? 1 : 0); writeUB(1, value.onTop ? 1 : 0); writeUB(4, value.passes); } /** * Writes GRADIENTGLOWFILTER value to the stream * * @param value GRADIENTGLOWFILTER value * @throws IOException */ public void writeGRADIENTGLOWFILTER(GRADIENTGLOWFILTER value) throws IOException { writeUI8(value.gradientColors.length); for (int i = 0; i < value.gradientColors.length; i++) { writeRGBA(value.gradientColors[i]); } for (int i = 0; i < value.gradientColors.length; i++) { writeUI8(value.gradientRatio[i]); } writeFIXED(value.blurX); writeFIXED(value.blurY); writeFIXED(value.angle); writeFIXED(value.distance); writeFIXED8(value.strength); writeUB(1, value.innerShadow ? 1 : 0); writeUB(1, value.knockout ? 1 : 0); writeUB(1, value.compositeSource ? 1 : 0); writeUB(1, value.onTop ? 1 : 0); writeUB(4, value.passes); } /** * Writes GRADIENTBEVELFILTER value to the stream * * @param value GRADIENTBEVELFILTER value * @throws IOException */ public void writeGRADIENTBEVELFILTER(GRADIENTBEVELFILTER value) throws IOException { writeUI8(value.gradientColors.length); for (int i = 0; i < value.gradientColors.length; i++) { writeRGBA(value.gradientColors[i]); } for (int i = 0; i < value.gradientColors.length; i++) { writeUI8(value.gradientRatio[i]); } writeFIXED(value.blurX); writeFIXED(value.blurY); writeFIXED(value.angle); writeFIXED(value.distance); writeFIXED8(value.strength); writeUB(1, value.innerShadow ? 1 : 0); writeUB(1, value.knockout ? 1 : 0); writeUB(1, value.compositeSource ? 1 : 0); writeUB(1, value.onTop ? 1 : 0); writeUB(4, value.passes); } /** * Writes list of FILTER values to the stream * * @param list List of FILTER values * @throws IOException */ public void writeFILTERLIST(List list) throws IOException { writeUI8(list.size()); for (int i = 0; i < list.size(); i++) { writeFILTER(list.get(i)); } } /** * Writes FILTER value to the stream * * @param value FILTER value * @throws IOException */ public void writeFILTER(FILTER value) throws IOException { writeUI8(value.id); switch (value.id) { case 0: writeDROPSHADOWFILTER((DROPSHADOWFILTER) value); break; case 1: writeBLURFILTER((BLURFILTER) value); break; case 2: writeGLOWFILTER((GLOWFILTER) value); break; case 3: writeBEVELFILTER((BEVELFILTER) value); break; case 4: writeGRADIENTGLOWFILTER((GRADIENTGLOWFILTER) value); break; case 5: writeCONVOLUTIONFILTER((CONVOLUTIONFILTER) value); break; case 6: writeCOLORMATRIXFILTER((COLORMATRIXFILTER) value); break; case 7: writeGRADIENTBEVELFILTER((GRADIENTBEVELFILTER) value); break; } } /** * Writes list of BUTTONRECORD values to the stream * * @param list List of BUTTONRECORD values * @param inDefineButton2 Whether write inside of DefineButton2Tag or not * @throws IOException */ public void writeBUTTONRECORDList(List list, boolean inDefineButton2) throws IOException { for (BUTTONRECORD brec : list) { writeBUTTONRECORD(brec, inDefineButton2); } writeUI8(0); } /** * Writes BUTTONRECORD value to the stream * * @param value BUTTONRECORD value * @param inDefineButton2 Whether write inside of DefineButton2Tag or not * @throws IOException */ public void writeBUTTONRECORD(BUTTONRECORD value, boolean inDefineButton2) throws IOException { writeUB(2, 0);//reserved writeUB(1, value.buttonHasBlendMode ? 1 : 0); writeUB(1, value.buttonHasFilterList ? 1 : 0); writeUB(1, value.buttonStateHitTest ? 1 : 0); writeUB(1, value.buttonStateDown ? 1 : 0); writeUB(1, value.buttonStateOver ? 1 : 0); writeUB(1, value.buttonStateUp ? 1 : 0); writeUI16(value.characterId); writeUI16(value.placeDepth); writeMatrix(value.placeMatrix); if (inDefineButton2) { writeCXFORMWITHALPHA(value.colorTransform); if (value.buttonHasFilterList) { writeFILTERLIST(value.filterList); } if (value.buttonHasBlendMode) { writeUI8(value.blendMode); } } } /** * Writes list of BUTTONCONDACTION values to the stream * * @param list List of BUTTONCONDACTION values * @throws IOException */ public void writeBUTTONCONDACTIONList(List list) throws IOException { for (int i = 0; i < list.size(); i++) { writeBUTTONCONDACTION(list.get(i), i == list.size() - 1); } } /** * Writes BUTTONCONDACTION value to the stream * * @param value BUTTONCONDACTION value * @param isLast True if it is last on the list * @throws IOException */ public void writeBUTTONCONDACTION(BUTTONCONDACTION value, boolean isLast) throws IOException { BUTTONCONDACTION ret = new BUTTONCONDACTION(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); SWFOutputStream sos = new SWFOutputStream(baos, version); sos.writeUB(1, value.condIdleToOverDown ? 1 : 0); sos.writeUB(1, value.condOutDownToIdle ? 1 : 0); sos.writeUB(1, value.condOutDownToOverDown ? 1 : 0); sos.writeUB(1, value.condOverDownToOutDown ? 1 : 0); sos.writeUB(1, value.condOverDownToOverUp ? 1 : 0); sos.writeUB(1, value.condOverUpToOverDown ? 1 : 0); sos.writeUB(1, value.condOverUpToIddle ? 1 : 0); sos.writeUB(1, value.condIdleToOverUp ? 1 : 0); sos.writeUB(7, value.condKeyPress); sos.writeUB(1, value.condOverDownToIddle ? 1 : 0); sos.write(value.actionBytes); //sos.write(Action.actionsToBytes(value.actions, true, version)); sos.close(); byte data[] = baos.toByteArray(); if (isLast) { writeUI16(0); } else { writeUI16(data.length + 2); } write(data); } }