/*
* Copyright (C) 2010-2016 JPEXS, All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library.
*/
package com.jpexs.decompiler.flash.tags;
import com.jpexs.decompiler.flash.SWF;
import com.jpexs.decompiler.flash.SWFInputStream;
import com.jpexs.decompiler.flash.SWFOutputStream;
import com.jpexs.decompiler.flash.configuration.Configuration;
import com.jpexs.decompiler.flash.exporters.commonshape.ExportRectangle;
import com.jpexs.decompiler.flash.exporters.commonshape.Matrix;
import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter;
import com.jpexs.decompiler.flash.helpers.HighlightedText;
import com.jpexs.decompiler.flash.helpers.HighlightedTextWriter;
import com.jpexs.decompiler.flash.helpers.hilight.HighlightSpecialType;
import com.jpexs.decompiler.flash.tags.base.BoundedTag;
import com.jpexs.decompiler.flash.tags.base.FontTag;
import com.jpexs.decompiler.flash.tags.base.MissingCharacterHandler;
import com.jpexs.decompiler.flash.tags.base.RenderContext;
import com.jpexs.decompiler.flash.tags.base.TextTag;
import com.jpexs.decompiler.flash.tags.dynamictext.CharacterWithStyle;
import com.jpexs.decompiler.flash.tags.dynamictext.DynamicTextModel;
import com.jpexs.decompiler.flash.tags.dynamictext.GlyphCharacter;
import com.jpexs.decompiler.flash.tags.dynamictext.Paragraph;
import com.jpexs.decompiler.flash.tags.dynamictext.SameStyleTextRecord;
import com.jpexs.decompiler.flash.tags.dynamictext.TextStyle;
import com.jpexs.decompiler.flash.tags.dynamictext.Word;
import com.jpexs.decompiler.flash.tags.enums.TextRenderMode;
import com.jpexs.decompiler.flash.tags.text.ParsedSymbol;
import com.jpexs.decompiler.flash.tags.text.TextAlign;
import com.jpexs.decompiler.flash.tags.text.TextLexer;
import com.jpexs.decompiler.flash.tags.text.TextParseException;
import com.jpexs.decompiler.flash.types.BasicType;
import com.jpexs.decompiler.flash.types.ColorTransform;
import com.jpexs.decompiler.flash.types.DynamicTextGlyphEntry;
import com.jpexs.decompiler.flash.types.MATRIX;
import com.jpexs.decompiler.flash.types.RECT;
import com.jpexs.decompiler.flash.types.RGB;
import com.jpexs.decompiler.flash.types.RGBA;
import com.jpexs.decompiler.flash.types.TEXTRECORD;
import com.jpexs.decompiler.flash.types.annotations.Conditional;
import com.jpexs.decompiler.flash.types.annotations.EnumValue;
import com.jpexs.decompiler.flash.types.annotations.SWFType;
import com.jpexs.decompiler.flash.types.annotations.SWFVersion;
import com.jpexs.helpers.ByteArrayRange;
import com.jpexs.helpers.SerializableImage;
import com.jpexs.helpers.utf8.Utf8Helper;
import java.awt.Color;
import java.awt.Font;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
*
* @author JPEXS
*/
@SWFVersion(from = 4)
public class DefineEditTextTag extends TextTag {
public static final int ID = 37;
public static final String NAME = "DefineEditText";
@SWFType(BasicType.UI16)
public int characterID;
public RECT bounds;
public boolean hasText;
public boolean wordWrap;
public boolean multiline;
public boolean password;
public boolean readOnly;
public boolean hasTextColor;
public boolean hasMaxLength;
public boolean hasFont;
public boolean hasFontClass;
public boolean autoSize;
public boolean hasLayout;
public boolean noSelect;
public boolean border;
public boolean wasStatic;
public boolean html;
public boolean useOutlines;
@SWFType(BasicType.UI16)
@Conditional("hasFont")
public int fontId;
@Conditional("hasFontClass")
public String fontClass;
@SWFType(BasicType.UI16)
@Conditional("hasFont|hasFontClass")
public int fontHeight;
@Conditional("hasTextColor")
public RGBA textColor;
@SWFType(BasicType.UI16)
@Conditional("hasMaxLength")
public int maxLength;
@SWFType(BasicType.UI8)
@Conditional("hasLayout")
@EnumValue(value = ALIGN_LEFT, text = "Left")
@EnumValue(value = ALIGN_RIGHT, text = "Right")
@EnumValue(value = ALIGN_CENTER, text = "Center")
@EnumValue(value = ALIGN_JUSTIFY, text = "Justify")
public int align;
@SWFType(BasicType.UI16)
@Conditional("hasLayout")
public int leftMargin;
@SWFType(BasicType.UI16)
@Conditional("hasLayout")
public int rightMargin;
@SWFType(BasicType.UI16)
@Conditional("hasLayout")
public int indent;
@SWFType(BasicType.SI16)
@Conditional("hasLayout")
public int leading;
public String variableName;
@Conditional("hasText")
public String initialText;
public static final int ALIGN_LEFT = 0;
public static final int ALIGN_RIGHT = 1;
public static final int ALIGN_CENTER = 2;
public static final int ALIGN_JUSTIFY = 3;
/**
* Constructor
*
* @param swf
*/
public DefineEditTextTag(SWF swf) {
super(swf, ID, NAME, null);
characterID = swf.getNextCharacterId();
bounds = new RECT();
variableName = "";
}
/**
* Constructor
*
* @param sis
* @param data
* @throws IOException
*/
public DefineEditTextTag(SWFInputStream sis, ByteArrayRange data) throws IOException {
super(sis.getSwf(), ID, NAME, data);
readData(sis, data, 0, false, false, false);
}
@Override
public final void readData(SWFInputStream sis, ByteArrayRange data, int level, boolean parallel, boolean skipUnusualTags, boolean lazy) throws IOException {
characterID = sis.readUI16("characterID");
bounds = sis.readRECT("bounds");
hasText = sis.readUB(1, "hasText") == 1;
wordWrap = sis.readUB(1, "wordWrap") == 1;
multiline = sis.readUB(1, "multiline") == 1;
password = sis.readUB(1, "password") == 1;
readOnly = sis.readUB(1, "readOnly") == 1;
hasTextColor = sis.readUB(1, "hasTextColor") == 1;
hasMaxLength = sis.readUB(1, "hasMaxLength") == 1;
hasFont = sis.readUB(1, "hasFont") == 1;
hasFontClass = sis.readUB(1, "hasFontClass") == 1;
autoSize = sis.readUB(1, "autoSize") == 1;
hasLayout = sis.readUB(1, "hasLayout") == 1;
noSelect = sis.readUB(1, "noSelect") == 1;
border = sis.readUB(1, "border") == 1;
wasStatic = sis.readUB(1, "wasStatic") == 1;
html = sis.readUB(1, "html") == 1;
useOutlines = sis.readUB(1, "useOutlines") == 1;
if (hasFont) {
fontId = sis.readUI16("fontId");
}
if (hasFontClass) {
fontClass = sis.readString("fontClass");
}
// condition is wrong in the documentation
if (hasFont || hasFontClass) {
fontHeight = sis.readUI16("fontHeight");
}
if (hasTextColor) {
textColor = sis.readRGBA("textColor");
}
if (hasMaxLength) {
maxLength = sis.readUI16("maxLength");
}
if (hasLayout) {
align = sis.readUI8("align"); //0 left, 1 right, 2 center, 3 justify
leftMargin = sis.readUI16("leftMargin");
rightMargin = sis.readUI16("rightMargin");
indent = sis.readUI16("indent");
leading = sis.readSI16("leading");
}
variableName = sis.readString("variableName");
if (hasText) {
initialText = sis.readString("initialText");
}
}
/**
* Gets data bytes
*
* @param sos SWF output stream
* @throws java.io.IOException
*/
@Override
public void getData(SWFOutputStream sos) throws IOException {
sos.writeUI16(characterID);
sos.writeRECT(bounds);
sos.writeUB(1, hasText ? 1 : 0);
sos.writeUB(1, wordWrap ? 1 : 0);
sos.writeUB(1, multiline ? 1 : 0);
sos.writeUB(1, password ? 1 : 0);
sos.writeUB(1, readOnly ? 1 : 0);
sos.writeUB(1, hasTextColor ? 1 : 0);
sos.writeUB(1, hasMaxLength ? 1 : 0);
sos.writeUB(1, hasFont ? 1 : 0);
sos.writeUB(1, hasFontClass ? 1 : 0);
sos.writeUB(1, autoSize ? 1 : 0);
sos.writeUB(1, hasLayout ? 1 : 0);
sos.writeUB(1, noSelect ? 1 : 0);
sos.writeUB(1, border ? 1 : 0);
sos.writeUB(1, wasStatic ? 1 : 0);
sos.writeUB(1, html ? 1 : 0);
sos.writeUB(1, useOutlines ? 1 : 0);
if (hasFont) {
sos.writeUI16(fontId);
}
if (hasFontClass) {
sos.writeString(fontClass);
}
if (hasFont || hasFontClass) {
sos.writeUI16(fontHeight);
}
if (hasTextColor) {
sos.writeRGBA(textColor);
}
if (hasMaxLength) {
sos.writeUI16(maxLength);
}
if (hasLayout) {
sos.writeUI8(align);
sos.writeUI16(leftMargin);
sos.writeUI16(rightMargin);
sos.writeUI16(indent);
sos.writeSI16(leading);
}
sos.writeString(variableName);
if (hasText) {
sos.writeString(initialText);
}
}
@Override
public RECT getBounds() {
return bounds;
}
@Override
public MATRIX getTextMatrix() {
MATRIX matrix = new MATRIX();
matrix.translateX = bounds.Xmin;
matrix.translateY = bounds.Ymin;
return matrix;
}
@Override
public void setBounds(RECT r) {
bounds = r;
}
private String stripTags(String inp) {
boolean intag = false;
String outp = "";
inp = inp.replaceAll("<br ?/?>", "\r\n");
for (int i = 0; i < inp.length(); ++i) {
if (!intag && inp.charAt(i) == '<') {
intag = true;
continue;
}
if (intag && inp.charAt(i) == '>') {
intag = false;
continue;
}
if (!intag) {
outp += inp.charAt(i);
}
}
return outp;
}
private String entitiesReplace(String s) {
s = s.replace("<", "<");
s = s.replace(">", ">");
s = s.replace("&", "&");
s = s.replace(""", "\"");
return s;
}
@Override
public List<String> getTexts() {
String ret = "";
if (hasText) {
ret = initialText;
}
if (html) {
ret = stripTags(ret);
ret = entitiesReplace(ret);
}
return Arrays.asList(ret);
}
private List<CharacterWithStyle> getTextWithStyle() {
String str = "";
TextStyle style = new TextStyle();
if (fontClass != null) {
style.font = swf.getFontByClass(fontClass);
} else {
style.font = swf.getFont(fontId);
}
style.fontHeight = fontHeight;
style.fontLeading = leading;
if (hasTextColor) {
style.textColor = textColor;
}
if (hasText) {
str = initialText;
}
final List<CharacterWithStyle> ret = new ArrayList<>();
if (html) {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser;
final Stack<TextStyle> styles = new Stack<>();
styles.add(style);
try {
saxParser = factory.newSAXParser();
DefaultHandler handler = new DefaultHandler() {
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
TextStyle style = styles.peek();
switch (qName) {
case "p":
// todo: parse the following attribute:
// align
break;
case "b":
style = style.clone();
style.bold = true;
styles.add(style);
break;
case "i":
style = style.clone();
style.italic = true;
styles.add(style);
break;
case "u":
style = style.clone();
style.underlined = true;
styles.add(style);
break;
case "font":
style = style.clone();
String color = attributes.getValue("color");
if (color != null) {
if (color.startsWith("#")) {
style.textColor = new RGBA(Color.decode(color));
}
}
String size = attributes.getValue("size");
if (size != null && size.length() > 0) {
char firstChar = size.charAt(0);
if (firstChar != '+' && firstChar != '-') {
int fontSize = Integer.parseInt(size);
style.fontHeight = (int) Math.round(fontSize * SWF.unitDivisor);
style.fontLeading = leading;
} else {
// todo: parse relative sizes
}
}
String face = attributes.getValue("face");
{
if (face != null && face.length() > 0) {
style.fontFace = face;
FontTag insideFont = swf.getFontByName(face);
style.font = insideFont;
if (insideFont != null) {
style.fontFace = null;
}
}
}
// todo: parse the following attributes: letterSpacing, kerning
styles.add(style);
break;
case "br":
case "sbr": // what's this?
CharacterWithStyle cs = new CharacterWithStyle();
cs.character = '\n';
cs.style = style;
ret.add(cs);
break;
}
//ret = entitiesReplace(ret);
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
switch (qName) {
case "b":
case "i":
case "u":
case "font":
styles.pop();
break;
case "p":
TextStyle style = styles.peek();
CharacterWithStyle cs = new CharacterWithStyle();
cs.character = '\n';
cs.style = style;
ret.add(cs);
break;
}
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
String txt = new String(ch, start, length);
TextStyle style = styles.peek();
addCharacters(ret, txt, style);
}
};
str = "<!DOCTYPE html [\n"
+ " <!ENTITY nbsp \" \"> \n"
+ "]><root>" + str + "</root>";
saxParser.parse(new ByteArrayInputStream(str.getBytes(Utf8Helper.charset)), handler);
} catch (ParserConfigurationException | SAXException | IOException ex) {
Logger.getLogger(DefineEditTextTag.class.getName()).log(Level.SEVERE, null, ex);
}
} else {
addCharacters(ret, str, style);
}
return ret;
}
private void addCharacters(List<CharacterWithStyle> list, String str, TextStyle style) {
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
CharacterWithStyle cs = new CharacterWithStyle();
cs.character = ch;
cs.style = style;
list.add(cs);
}
}
@Override
public List<Integer> getFontIds() {
List<Integer> ret = new ArrayList<>();
ret.add(fontId);
return ret;
}
@Override
public HighlightedText getFormattedText(boolean ignoreLetterSpacing) {
HighlightedTextWriter writer = new HighlightedTextWriter(Configuration.getCodeFormatting(), true);
writer.append("[");
String[] alignNames = {"left", "right", "center", "justify"};
String alignment;
if (align < alignNames.length) {
alignment = alignNames[align];
} else {
alignment = "unknown";
}
writer.newLine();
writer.append("xmin ").append(bounds.Xmin).newLine();
writer.append("ymin ").append(bounds.Ymin).newLine();
writer.append("xmax ").append(bounds.Xmax).newLine();
writer.append("ymax ").append(bounds.Ymax).newLine();
if (wordWrap) {
writer.append("wordwrap 1").newLine();
}
if (multiline) {
writer.append("multiline 1").newLine();
}
if (password) {
writer.append("password 1").newLine();
}
if (readOnly) {
writer.append("readonly 1").newLine();
}
if (autoSize) {
writer.append("autosize 1").newLine();
}
if (noSelect) {
writer.append("noselect 1").newLine();
}
if (border) {
writer.append("border 1").newLine();
}
if (wasStatic) {
writer.append("wasstatic 1").newLine();
}
if (html) {
writer.append("html 1").newLine();
}
if (useOutlines) {
writer.append("useoutlines 1").newLine();
}
if (hasFont) {
writer.append("font ").append(fontId).newLine();
writer.append("height ").append(fontHeight).newLine();
}
if (hasTextColor) {
writer.append("color ").append(textColor.toHexARGB()).newLine();
}
if (hasFontClass) {
writer.append("fontclass ").append(fontClass).newLine();
}
if (hasMaxLength) {
writer.append("maxlength ").append(maxLength).newLine();
}
writer.append("align ").append(alignment).newLine();
if (hasLayout) {
writer.append("leftmargin ").append(leftMargin).newLine();
writer.append("rightmargin ").append(rightMargin).newLine();
writer.append("indent ").append(indent).newLine();
writer.append("leading ").append(leading).newLine();
}
if (!variableName.isEmpty()) {
writer.append("variablename ").append(variableName).newLine();
}
writer.append("]");
if (hasText) {
String text = initialText.replace("\\", "\\\\").replace("[", "\\[").replace("]", "\\]");
writer.hilightSpecial(text, HighlightSpecialType.TEXT);
}
return new HighlightedText(writer);
}
@Override
public boolean setFormattedText(MissingCharacterHandler missingCharHandler, String formattedText, String[] texts) throws TextParseException {
try {
TextLexer lexer = new TextLexer(new StringReader(formattedText));
ParsedSymbol s;
formattedText = "";
RECT bounds = new RECT(this.bounds);
boolean wordWrap = false;
boolean multiline = false;
boolean password = false;
boolean readOnly = false;
boolean autoSize = false;
boolean noSelect = false;
boolean border = false;
boolean wasStatic = false;
boolean html = false;
boolean useOutlines = false;
int fontId = -1;
int fontHeight = -1;
String fontClass = null;
RGBA textColor = null;
int maxLength = -1;
int align = -1;
int leftMargin = -1;
int rightMargin = -1;
int indent = -1;
int leading = -1;
String variableName = null;
int textIdx = 0;
while ((s = lexer.yylex()) != null) {
switch (s.type) {
case PARAMETER:
String paramName = (String) s.values[0];
String paramValue = (String) s.values[1];
switch (paramName) {
case "xmin":
try {
bounds.Xmin = Integer.parseInt(paramValue);
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid xmin value. Number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "ymin":
try {
bounds.Ymin = Integer.parseInt(paramValue);
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid ymin value. Number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "xmax":
try {
bounds.Xmax = Integer.parseInt(paramValue);
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid xmax value. Number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "ymax":
try {
bounds.Ymax = Integer.parseInt(paramValue);
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid ymax value. Number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "wordwrap":
if (paramValue.equals("1")) {
wordWrap = true;
}
break;
case "multiline":
if (paramValue.equals("1")) {
multiline = true;
}
break;
case "password":
if (paramValue.equals("1")) {
password = true;
}
break;
case "readonly":
if (paramValue.equals("1")) {
readOnly = true;
}
break;
case "autosize":
if (paramValue.equals("1")) {
autoSize = true;
}
break;
case "noselect":
if (paramValue.equals("1")) {
noSelect = true;
}
break;
case "border":
if (paramValue.equals("1")) {
border = true;
}
break;
case "wasstatic":
if (paramValue.equals("1")) {
wasStatic = true;
}
break;
case "html":
if (paramValue.equals("1")) {
html = true;
}
break;
case "useoutlines":
if (paramValue.equals("1")) {
useOutlines = true;
}
break;
case "font"://note: height parameter must be also present
try {
fontId = Integer.parseInt(paramValue);
FontTag ft = swf.getFont(fontId);
if (ft == null) {
throw new TextParseException("Font not found.", lexer.yyline());
}
hasFont = true;
} catch (NumberFormatException ne) {
throw new TextParseException("Invalid font value. Number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "fontclass":
fontClass = paramValue;
break;
case "height":
try {//TODO: font parameter must be also present
fontHeight = Integer.parseInt(paramValue);
hasFont = true;
} catch (NumberFormatException ne) {
throw new TextParseException("Invalid height value. Number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "color":
Matcher m = Pattern.compile("#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])").matcher(paramValue);
if (m.matches()) {
textColor = new RGBA(Integer.parseInt(m.group(2), 16), Integer.parseInt(m.group(3), 16), Integer.parseInt(m.group(4), 16), Integer.parseInt(m.group(1), 16));
} else {
throw new TextParseException("Invalid color. Valid format is #aarrggbb. Found: " + paramValue, lexer.yyline());
}
break;
case "maxlength":
try {
maxLength = Integer.parseInt(paramValue);
} catch (NumberFormatException ne) {
throw new TextParseException("Invalid maxLength value. Number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "align":
switch (paramValue) {
case "left":
align = 0;
break;
case "right":
align = 1;
break;
case "center":
align = 2;
break;
case "justify":
align = 3;
break;
default:
throw new TextParseException("Invalid align value. Expected one of: left,right,center or justify. Found: " + paramValue, lexer.yyline());
}
break;
case "leftmargin":
try {
leftMargin = Integer.parseInt(paramValue);
} catch (NumberFormatException ne) {
throw new TextParseException("Invalid leftmargin value. Number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "rightmargin":
try {
rightMargin = Integer.parseInt(paramValue);
} catch (NumberFormatException ne) {
throw new TextParseException("Invalid rightmargin value. Number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "indent":
try {
indent = Integer.parseInt(paramValue);
} catch (NumberFormatException ne) {
throw new TextParseException("Invalid indent value. Number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "leading":
try {
leading = Integer.parseInt(paramValue);
} catch (NumberFormatException ne) {
throw new TextParseException("Invalid leading value. Number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "variablename":
variableName = paramValue;
break;
default:
throw new TextParseException("Unrecognized parameter name: " + paramName, lexer.yyline());
}
break;
case TEXT:
String s2 = (String) s.values[0];
if (s2 == null) {
s2 = "";
}
formattedText += (texts == null || textIdx >= texts.length) ? s2 : texts[textIdx++];
formattedText = formattedText.replace("\r\n", "\r");
break;
}
}
setModified(true);
this.bounds = bounds;
if (formattedText.length() > 0) {
initialText = formattedText;
this.hasText = true;
} else {
this.hasText = false;
}
this.wordWrap = wordWrap;
this.multiline = multiline;
this.password = password;
this.readOnly = readOnly;
this.noSelect = noSelect;
this.border = border;
this.wasStatic = wasStatic;
this.html = html;
this.useOutlines = useOutlines;
if (textColor != null) {
hasTextColor = true;
this.textColor = textColor;
} else {
hasTextColor = false;
}
if (maxLength > -1) {
this.maxLength = maxLength;
hasMaxLength = true;
} else {
hasMaxLength = false;
}
if (fontId > -1) {
this.fontId = fontId;
}
if (fontHeight > -1) {
this.fontHeight = fontHeight;
}
if (fontClass != null) {
this.fontClass = fontClass;
hasFontClass = true;
} else {
hasFontClass = false;
}
this.autoSize = autoSize;
this.align = align;
if (leftMargin > -1 || rightMargin > -1 || indent > -1 || leading > -1) {
this.leftMargin = leftMargin;
this.rightMargin = rightMargin;
this.indent = indent;
this.leading = leading;
hasLayout = true;
} else {
hasLayout = false;
}
if (variableName == null) {
variableName = "";
}
this.variableName = variableName;
} catch (IOException ex) {
Logger.getLogger(DefineEditTextTag.class.getName()).log(Level.SEVERE, null, ex);
return false;
}
return true;
}
@Override
public void updateTextBounds() {
}
@Override
public boolean alignText(TextAlign textAlign) {
return true;
}
@Override
public boolean translateText(int diff) {
return true;
}
@Override
public RECT getRect(Set<BoundedTag> added) {
return bounds;
}
@Override
public int getCharacterId() {
return characterID;
}
@Override
public void setCharacterId(int characterId) {
this.characterID = characterId;
}
@Override
public void getNeededCharacters(Set<Integer> needed) {
if (hasFont) {
needed.add(fontId);
}
if (html && hasText) {
List<CharacterWithStyle> chs = getTextWithStyle();
for (CharacterWithStyle ch : chs) {
if (ch.style.font != null) {
needed.add(ch.style.font.getFontId());
}
}
}
}
@Override
public boolean replaceCharacter(int oldCharacterId, int newCharacterId) {
if (fontId == oldCharacterId) {
fontId = newCharacterId;
setModified(true);
return true;
}
return false;
}
@Override
public boolean removeCharacter(int characterId) {
if (fontId == characterId) {
hasFont = false;
fontId = 0;
setModified(true);
return true;
}
return false;
}
@Override
public int getUsedParameters() {
return 0;
}
@Override
public void toImage(int frame, int time, int ratio, RenderContext renderContext, SerializableImage image, boolean isClip, Matrix transformation, Matrix strokeTransformation, Matrix absoluteTransformation, ColorTransform colorTransform) {
render(TextRenderMode.BITMAP, image, null, null, transformation, colorTransform, 1);
}
@Override
public void toSVG(SVGExporter exporter, int ratio, ColorTransform colorTransform, int level) {
render(TextRenderMode.SVG, null, exporter, null, new Matrix(), colorTransform, 1);
}
@Override
public void toHtmlCanvas(StringBuilder result, double unitDivisor) {
render(TextRenderMode.HTML5_CANVAS, null, null, result, new Matrix(), null, unitDivisor);
}
private void render(TextRenderMode renderMode, SerializableImage image, SVGExporter svgExporter, StringBuilder htmlCanvasBuilder, Matrix transformation, ColorTransform colorTransform, double zoom) {
if (border) {
// border is always black, fill color is always white?
RGB borderColor = new RGBA(Color.black);
RGB fillColor = new RGBA(Color.white);
switch (renderMode) {
case BITMAP:
drawBorder(swf, image, borderColor, fillColor, getRect(), getTextMatrix(), transformation, colorTransform);
break;
case HTML5_CANVAS:
drawBorderHtmlCanvas(swf, htmlCanvasBuilder, borderColor, fillColor, getRect(), getTextMatrix(), colorTransform, zoom);
break;
case SVG:
drawBorderSVG(swf, svgExporter, borderColor, fillColor, getRect(), getTextMatrix(), colorTransform, zoom);
break;
}
}
if (hasText) {
DynamicTextModel textModel = new DynamicTextModel();
List<CharacterWithStyle> txt = getTextWithStyle();
TextStyle lastStyle = null;
char prevChar = 0;
boolean lastWasWhiteSpace = false;
for (int i = 0; i < txt.size(); i++) {
CharacterWithStyle cs = txt.get(i);
char c = cs.character;
if (c != '\r' && c != '\n') {
// create new SameStyleTextRecord for all words and all diffrent style text parts
if (lastWasWhiteSpace && !Character.isWhitespace(c)) {
textModel.newWord();
lastWasWhiteSpace = false;
}
if (cs.style != lastStyle) {
lastStyle = cs.style;
textModel.style = lastStyle;
textModel.newRecord();
}
Character nextChar = null;
if (i + 1 < txt.size()) {
nextChar = txt.get(i + 1).character;
}
FontTag font = lastStyle.font;
DynamicTextGlyphEntry ge = new DynamicTextGlyphEntry();
ge.fontFace = lastStyle.fontFace;
if (ge.fontFace == null && font != null) {
ge.fontFace = font.getFontName();
}
ge.fontStyle = (lastStyle.bold ? Font.BOLD : 0) | (lastStyle.italic ? Font.ITALIC : 0);
ge.character = c;
ge.glyphIndex = -1; // always use system character glyphs in edit text
String fontName = ge.fontFace != null ? ge.fontFace : FontTag.defaultFontName;
int fontStyle = font == null ? ge.fontStyle : font.getFontStyle();
ge.glyphAdvance = (int) Math.round(SWF.unitDivisor * FontTag.getSystemFontAdvance(fontName, fontStyle, (int) (lastStyle.fontHeight / SWF.unitDivisor), c, nextChar));
textModel.addGlyph(c, ge);
if (Character.isWhitespace(c)) {
lastWasWhiteSpace = true;
}
} else if (multiline) {
textModel.newParagraph();
}
prevChar = c;
}
textModel.calculateTextWidths();
List<List<SameStyleTextRecord>> lines;
if (multiline && wordWrap) {
lines = new ArrayList<>();
for (Paragraph paragraph : textModel.paragraphs) {
List<SameStyleTextRecord> line = new ArrayList<>();
int lineLength = 0;
for (Word word : paragraph.words) {
if (lineLength + word.width <= bounds.getWidth()) {
line.addAll(word.records);
lineLength += word.width;
} else {
lines.add(line);
line = new ArrayList<>();
line.addAll(word.records);
lineLength = 0;
}
}
if (!line.isEmpty()) {
lines.add(line);
}
}
} else {
lines = new ArrayList<>();
for (Paragraph paragraph : textModel.paragraphs) {
List<SameStyleTextRecord> line = new ArrayList<>();
for (Word word : paragraph.words) {
for (SameStyleTextRecord tr : word.records) {
line.add(tr);
}
}
lines.add(line);
}
}
// remove spaces after last word
for (List<SameStyleTextRecord> line : lines) {
boolean removed = true;
while (removed) {
removed = false;
while (line.size() > 0 && line.get(line.size() - 1).glyphEntries.isEmpty()) {
line.remove(line.size() - 1);
removed = true;
}
if (line.size() > 0) {
SameStyleTextRecord lastRecord = line.get(line.size() - 1);
while (lastRecord.glyphEntries.size() > 0
&& Character.isWhitespace(lastRecord.glyphEntries.get(lastRecord.glyphEntries.size() - 1).character)) {
lastRecord.glyphEntries.remove(lastRecord.glyphEntries.size() - 1);
removed = true;
}
}
}
}
textModel.calculateTextWidths();
List<TEXTRECORD> allTextRecords = new ArrayList<>();
int lastHeight = 0;
int yOffset = -leading;
for (List<SameStyleTextRecord> line : lines) {
int width = 0;
int currentOffset = 0;
if (line.isEmpty()) {
currentOffset = lastHeight;
} else {
for (SameStyleTextRecord tr : line) {
width += tr.width;
int lineHeight = tr.style.fontHeight + tr.style.fontLeading;
lastHeight = lineHeight;
if (lineHeight > currentOffset) {
currentOffset = lineHeight;
}
}
}
yOffset += currentOffset;
int alignOffset = 0;
switch (align) {
case ALIGN_LEFT:
alignOffset = 0;
break;
case ALIGN_RIGHT:
alignOffset = bounds.getWidth() - width;
break;
case ALIGN_CENTER:
alignOffset = (bounds.getWidth() - width) / 2;
break;
case ALIGN_JUSTIFY:
// todo;
break;
}
for (SameStyleTextRecord tr : line) {
tr.xOffset = alignOffset;
alignOffset += tr.width;
}
for (SameStyleTextRecord tr : line) {
TEXTRECORD tr2 = new TEXTRECORD();
int fid = fontId;
if (fontClass != null) {
FontTag ft = swf.getFontByClass(fontClass);
if (ft != null) {
fid = ft.getFontId();
}
}
if (tr.style.font != null) {
fid = tr.style.font.getFontId();
}
tr2.styleFlagsHasFont = fid != 0;
tr2.fontId = fid;
tr2.textHeight = tr.style.fontHeight;
if (tr.style.textColor != null) {
tr2.styleFlagsHasColor = true;
tr2.textColorA = tr.style.textColor;
}
// always add xOffset, because no xOffset and 0 xOffset is diffrent in text rendering
tr2.styleFlagsHasXOffset = true;
tr2.xOffset = tr.xOffset;
if (yOffset != 0) {
tr2.styleFlagsHasYOffset = true;
tr2.yOffset = yOffset;
}
tr2.glyphEntries = new ArrayList<>(tr.glyphEntries.size());
for (GlyphCharacter ge : tr.glyphEntries) {
tr2.glyphEntries.add(ge.glyphEntry);
}
allTextRecords.add(tr2);
}
}
switch (renderMode) {
case BITMAP:
staticTextToImage(swf, allTextRecords, 2, image, getTextMatrix(), transformation, colorTransform);
break;
case HTML5_CANVAS:
staticTextToHtmlCanvas(zoom, swf, allTextRecords, 2, htmlCanvasBuilder, getBounds(), getTextMatrix(), colorTransform);
break;
case SVG:
staticTextToSVG(swf, allTextRecords, 2, svgExporter, getBounds(), getTextMatrix(), colorTransform, zoom);
break;
}
}
}
@Override
public ExportRectangle calculateTextBounds() {
return null;
}
@Override
public int getNumFrames() {
return 1;
}
@Override
public boolean isSingleFrame() {
return true;
}
}