From 357d35ed17596f110f7e186634c2e7500a9fe837 Mon Sep 17 00:00:00 2001 From: "honfika@gmail.com" Date: Fri, 15 May 2015 12:57:04 +0200 Subject: [PATCH] parse "face" attributes of font tags in DefineEditTag, and render the text when glyph is not found in swf, too --- .../flash/tags/DefineEditTextTag.java | 2281 +++++++++-------- .../decompiler/flash/tags/base/TextTag.java | 18 +- .../flash/tags/dynamictext/TextStyle.java | 11 +- .../flash/types/DynamicTextGlyphEntry.java | 30 + 4 files changed, 1197 insertions(+), 1143 deletions(-) create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/DynamicTextGlyphEntry.java diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineEditTextTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineEditTextTag.java index 0502dfcd6..64052703c 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineEditTextTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineEditTextTag.java @@ -1,1136 +1,1145 @@ -/* - * Copyright (C) 2010-2015 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.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.GLYPHENTRY; -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.SWFType; -import com.jpexs.helpers.ByteArrayRange; -import com.jpexs.helpers.SerializableImage; -import com.jpexs.helpers.utf8.Utf8Helper; -import java.awt.Color; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -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 - */ -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") - public int fontHeight; - - @Conditional("hasTextColor") - public RGBA textColor; - - @SWFType(BasicType.UI16) - @Conditional("hasMaxLength") - public int maxLength; - - @SWFType(BasicType.UI8) - @Conditional("hasLayout") - 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; - - @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("
", "\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 getTexts() { - String ret = ""; - if (hasText) { - ret = initialText; - } - if (html) { - ret = stripTags(ret); - ret = entitiesReplace(ret); - } - return Arrays.asList(ret); - } - - private List getTextWithStyle() { - String str = ""; - TextStyle style = new TextStyle(); - style.font = swf.getFont(fontId); - style.fontHeight = fontHeight; - style.fontLeading = leading; - if (hasTextColor) { - style.textColor = textColor; - } - if (hasText) { - str = initialText; - } - final List ret = new ArrayList<>(); - if (html) { - SAXParserFactory factory = SAXParserFactory.newInstance(); - SAXParser saxParser; - final Stack 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 * (style.font == null ? 1 : style.font.getDivider())); - style.fontLeading = leading; - } else { - // todo: parse relative sizes - } - } - // todo: parse the following attributes: - // face, 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 = " \n" - + "]>" + str + ""; - 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 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 getFontIds() { - List ret = new ArrayList<>(); - ret.add(fontId); - return ret; - } - - @Override - public HighlightedText getFormattedText() { - 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 " + bounds.Xmin).newLine(); - writer.append("ymin " + bounds.Ymin).newLine(); - writer.append("xmax " + bounds.Xmax).newLine(); - writer.append("ymax " + 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 " + fontId).newLine(); - writer.append("height " + fontHeight).newLine(); - } - if (hasTextColor) { - writer.append("color " + textColor.toHexARGB()).newLine(); - } - if (hasFontClass) { - writer.append("fontclass " + fontClass).newLine(); - } - if (hasMaxLength) { - writer.append("maxlength " + maxLength).newLine(); - } - writer.append("align " + alignment).newLine(); - if (hasLayout) { - writer.append("leftmargin " + leftMargin).newLine(); - writer.append("rightmargin " + rightMargin).newLine(); - writer.append("indent " + indent).newLine(); - writer.append("leading " + leading).newLine(); - } - if (!variableName.isEmpty()) { - writer.append("variablename " + 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 = null; - 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": - try { - fontId = Integer.parseInt(paramValue); - - FontTag ft = swf.getFont(fontId); - if (ft == null) { - throw new TextParseException("Font not found.", lexer.yyline()); - } - } catch (NumberFormatException ne) { - throw new TextParseException("Invalid font value. Number expected. Found: " + paramValue, lexer.yyline()); - } - break; - case "fontclass": - fontClass = paramValue; - break; - case "height": - try { - fontHeight = Integer.parseInt(paramValue); - } 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; - } - if (maxLength > -1) { - this.maxLength = maxLength; - hasMaxLength = true; - } - if (fontId > -1) { - this.fontId = fontId; - } - if (fontHeight > -1) { - this.fontHeight = fontHeight; - } - if (fontClass != null) { - this.fontClass = fontClass; - hasFontClass = true; - } - 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; - } - 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 added) { - return bounds; - } - - @Override - public int getCharacterId() { - return characterID; - } - - @Override - public void setCharacterId(int characterId) { - this.characterID = characterId; - } - - /** - * Gets data bytes - * - * @return Bytes of data - */ - @Override - public byte[] getData() { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - OutputStream os = baos; - SWFOutputStream sos = new SWFOutputStream(os, getVersion()); - try { - 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) { - 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); - } - - } catch (IOException e) { - throw new Error("This should never happen.", e); - } - return baos.toByteArray(); - } - - /** - * 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"); - } - if (hasFont) { - 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"); - } - - } - - @Override - public void getNeededCharacters(Set needed) { - if (hasFont) { - needed.add(fontId); - } - } - - @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, Matrix transformation, ColorTransform colorTransform) { - render(false, image, transformation, colorTransform); - } - - @Override - public void toSVG(SVGExporter exporter, int ratio, ColorTransform colorTransform, int level, double zoom) { - // todo: implement - } - - @Override - public String toHtmlCanvas(double unitDivisor) { - return render(true, null, new Matrix(), new ColorTransform()); - } - - private String render(boolean canvas, SerializableImage image, Matrix transformation, ColorTransform colorTransform) { - if (border) { - // border is always black, fill color is always white? - RGB borderColor = new RGBA(Color.black); - RGB fillColor = new RGBA(Color.white); - if (!canvas) { - drawBorder(swf, image, borderColor, fillColor, getRect(), getTextMatrix(), transformation, colorTransform); - } else { - // TODO: draw border - } - } - if (hasText) { - DynamicTextModel textModel = new DynamicTextModel(); - List 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; - } - int advance; - FontTag font = lastStyle.font; - GLYPHENTRY ge = new GLYPHENTRY(); - ge.glyphIndex = font == null ? -1 : font.charToGlyph(c); - if (font != null && font.hasLayout()) { - int kerningAdjustment = 0; - if (nextChar != null) { - kerningAdjustment = font.getCharKerningAdjustment(c, nextChar); - kerningAdjustment /= font.getDivider(); - } - advance = (int) Math.round(Math.round((double) lastStyle.fontHeight * (font.getGlyphAdvance(ge.glyphIndex) + kerningAdjustment) / (font.getDivider() * 1024.0))); - } else { - String fontName = FontTag.defaultFontName; - int fontStyle = font == null ? 0 : font.getFontStyle(); - advance = (int) Math.round(SWF.unitDivisor * FontTag.getSystemFontAdvance(fontName, fontStyle, (int) (lastStyle.fontHeight / SWF.unitDivisor), c, nextChar)); - } - ge.glyphAdvance = advance; - textModel.addGlyph(c, ge); - if (Character.isWhitespace(c)) { - lastWasWhiteSpace = true; - } - } else { - if (multiline) { - textModel.newParagraph(); - } - } - prevChar = c; - } - - textModel.calculateTextWidths(); - List> lines; - if (multiline && wordWrap) { - lines = new ArrayList<>(); - for (Paragraph paragraph : textModel.paragraphs) { - List 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 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 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 allTextRecords = new ArrayList<>(); - int lastHeight = 0; - int yOffset = 0; - for (List 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 0: // left - alignOffset = 0; - break; - case 1: // right - alignOffset = bounds.getWidth() - width; - break; - case 2: // center - alignOffset = (bounds.getWidth() - width) / 2; - break; - case 3: // justify - // todo; - break; - } - for (SameStyleTextRecord tr : line) { - tr.xOffset = alignOffset; - alignOffset += tr.width; - } - for (SameStyleTextRecord tr : line) { - TEXTRECORD tr2 = new TEXTRECORD(); - tr2.styleFlagsHasFont = fontId != 0; - tr2.fontId = fontId; - 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); - } - } - - if (canvas) { - return staticTextToHtmlCanvas(1, swf, allTextRecords, 2, getBounds(), getTextMatrix(), colorTransform); - } else { - staticTextToImage(swf, allTextRecords, 2, image, getTextMatrix(), transformation, colorTransform); - } - } - - return ""; - } - - @Override - public ExportRectangle calculateTextBounds() { - return null; - } - - @Override - public int getNumFrames() { - return 1; - } - - @Override - public boolean isSingleFrame() { - return true; - } -} +/* + * Copyright (C) 2010-2015 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.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.SWFType; +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.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +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 + */ +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") + public int fontHeight; + + @Conditional("hasTextColor") + public RGBA textColor; + + @SWFType(BasicType.UI16) + @Conditional("hasMaxLength") + public int maxLength; + + @SWFType(BasicType.UI8) + @Conditional("hasLayout") + 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; + + @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("
", "\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 getTexts() { + String ret = ""; + if (hasText) { + ret = initialText; + } + if (html) { + ret = stripTags(ret); + ret = entitiesReplace(ret); + } + return Arrays.asList(ret); + } + + private List getTextWithStyle() { + String str = ""; + TextStyle style = new TextStyle(); + style.font = swf.getFont(fontId); + style.fontHeight = fontHeight; + style.fontLeading = leading; + if (hasTextColor) { + style.textColor = textColor; + } + if (hasText) { + str = initialText; + } + final List ret = new ArrayList<>(); + if (html) { + SAXParserFactory factory = SAXParserFactory.newInstance(); + SAXParser saxParser; + final Stack 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 * (style.font == null ? 1 : style.font.getDivider())); + style.fontLeading = leading; + } else { + // todo: parse relative sizes + } + } + String face = attributes.getValue("face"); + { + if (face != null && face.length() > 0) { + style.fontFace = face; + } + } + // 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 = " \n" + + "]>" + str + ""; + 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 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 getFontIds() { + List ret = new ArrayList<>(); + ret.add(fontId); + return ret; + } + + @Override + public HighlightedText getFormattedText() { + 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 " + bounds.Xmin).newLine(); + writer.append("ymin " + bounds.Ymin).newLine(); + writer.append("xmax " + bounds.Xmax).newLine(); + writer.append("ymax " + 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 " + fontId).newLine(); + writer.append("height " + fontHeight).newLine(); + } + if (hasTextColor) { + writer.append("color " + textColor.toHexARGB()).newLine(); + } + if (hasFontClass) { + writer.append("fontclass " + fontClass).newLine(); + } + if (hasMaxLength) { + writer.append("maxlength " + maxLength).newLine(); + } + writer.append("align " + alignment).newLine(); + if (hasLayout) { + writer.append("leftmargin " + leftMargin).newLine(); + writer.append("rightmargin " + rightMargin).newLine(); + writer.append("indent " + indent).newLine(); + writer.append("leading " + leading).newLine(); + } + if (!variableName.isEmpty()) { + writer.append("variablename " + 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 = null; + 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": + try { + fontId = Integer.parseInt(paramValue); + + FontTag ft = swf.getFont(fontId); + if (ft == null) { + throw new TextParseException("Font not found.", lexer.yyline()); + } + } catch (NumberFormatException ne) { + throw new TextParseException("Invalid font value. Number expected. Found: " + paramValue, lexer.yyline()); + } + break; + case "fontclass": + fontClass = paramValue; + break; + case "height": + try { + fontHeight = Integer.parseInt(paramValue); + } 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; + } + if (maxLength > -1) { + this.maxLength = maxLength; + hasMaxLength = true; + } + if (fontId > -1) { + this.fontId = fontId; + } + if (fontHeight > -1) { + this.fontHeight = fontHeight; + } + if (fontClass != null) { + this.fontClass = fontClass; + hasFontClass = true; + } + 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; + } + 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 added) { + return bounds; + } + + @Override + public int getCharacterId() { + return characterID; + } + + @Override + public void setCharacterId(int characterId) { + this.characterID = characterId; + } + + /** + * Gets data bytes + * + * @return Bytes of data + */ + @Override + public byte[] getData() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + OutputStream os = baos; + SWFOutputStream sos = new SWFOutputStream(os, getVersion()); + try { + 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) { + 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); + } + + } catch (IOException e) { + throw new Error("This should never happen.", e); + } + return baos.toByteArray(); + } + + /** + * 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"); + } + if (hasFont) { + 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"); + } + + } + + @Override + public void getNeededCharacters(Set needed) { + if (hasFont) { + needed.add(fontId); + } + } + + @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, Matrix transformation, ColorTransform colorTransform) { + render(false, image, transformation, colorTransform); + } + + @Override + public void toSVG(SVGExporter exporter, int ratio, ColorTransform colorTransform, int level, double zoom) { + // todo: implement + } + + @Override + public String toHtmlCanvas(double unitDivisor) { + return render(true, null, new Matrix(), new ColorTransform()); + } + + private String render(boolean canvas, SerializableImage image, Matrix transformation, ColorTransform colorTransform) { + if (border) { + // border is always black, fill color is always white? + RGB borderColor = new RGBA(Color.black); + RGB fillColor = new RGBA(Color.white); + if (!canvas) { + drawBorder(swf, image, borderColor, fillColor, getRect(), getTextMatrix(), transformation, colorTransform); + } else { + // TODO: draw border + } + } + if (hasText) { + DynamicTextModel textModel = new DynamicTextModel(); + List 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; + } + int advance; + FontTag font = lastStyle.font; + DynamicTextGlyphEntry ge = new DynamicTextGlyphEntry(); + ge.fontFace = lastStyle.fontFace; + ge.fontStyle = (lastStyle.bold ? Font.BOLD : 0) | (lastStyle.italic ? Font.ITALIC : 0); + ge.character = c; + ge.glyphIndex = font == null ? -1 : font.charToGlyph(c); + if (font != null && font.hasLayout()) { + int kerningAdjustment = 0; + if (nextChar != null) { + kerningAdjustment = font.getCharKerningAdjustment(c, nextChar); + kerningAdjustment /= font.getDivider(); + } + advance = (int) Math.round(Math.round((double) lastStyle.fontHeight * (font.getGlyphAdvance(ge.glyphIndex) + kerningAdjustment) / (font.getDivider() * 1024.0))); + } else { + String fontName = lastStyle.fontFace != null ? lastStyle.fontFace : FontTag.defaultFontName; + int fontStyle = font == null ? ge.fontStyle : font.getFontStyle(); + advance = (int) Math.round(SWF.unitDivisor * FontTag.getSystemFontAdvance(fontName, fontStyle, (int) (lastStyle.fontHeight / SWF.unitDivisor), c, nextChar)); + } + ge.glyphAdvance = advance; + textModel.addGlyph(c, ge); + if (Character.isWhitespace(c)) { + lastWasWhiteSpace = true; + } + } else { + if (multiline) { + textModel.newParagraph(); + } + } + prevChar = c; + } + + textModel.calculateTextWidths(); + List> lines; + if (multiline && wordWrap) { + lines = new ArrayList<>(); + for (Paragraph paragraph : textModel.paragraphs) { + List 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 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 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 allTextRecords = new ArrayList<>(); + int lastHeight = 0; + int yOffset = 0; + for (List 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 0: // left + alignOffset = 0; + break; + case 1: // right + alignOffset = bounds.getWidth() - width; + break; + case 2: // center + alignOffset = (bounds.getWidth() - width) / 2; + break; + case 3: // justify + // todo; + break; + } + for (SameStyleTextRecord tr : line) { + tr.xOffset = alignOffset; + alignOffset += tr.width; + } + for (SameStyleTextRecord tr : line) { + TEXTRECORD tr2 = new TEXTRECORD(); + tr2.styleFlagsHasFont = fontId != 0; + tr2.fontId = fontId; + 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); + } + } + + if (canvas) { + return staticTextToHtmlCanvas(1, swf, allTextRecords, 2, getBounds(), getTextMatrix(), colorTransform); + } else { + staticTextToImage(swf, allTextRecords, 2, image, getTextMatrix(), transformation, colorTransform); + } + } + + return ""; + } + + @Override + public ExportRectangle calculateTextBounds() { + return null; + } + + @Override + public int getNumFrames() { + return 1; + } + + @Override + public boolean isSingleFrame() { + return true; + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/TextTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/TextTag.java index 4409306d2..8463c3803 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/TextTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/TextTag.java @@ -32,6 +32,7 @@ import com.jpexs.decompiler.flash.tags.text.JustifyAlignGlyphEntry; import com.jpexs.decompiler.flash.tags.text.TextAlign; import com.jpexs.decompiler.flash.tags.text.TextParseException; import com.jpexs.decompiler.flash.types.ColorTransform; +import com.jpexs.decompiler.flash.types.DynamicTextGlyphEntry; import com.jpexs.decompiler.flash.types.FILLSTYLE; import com.jpexs.decompiler.flash.types.FILLSTYLEARRAY; import com.jpexs.decompiler.flash.types.GLYPHENTRY; @@ -424,7 +425,8 @@ public abstract class TextTag extends CharacterTag implements DrawableTag { y = rec.yOffset; } - double rat = textHeight / 1024.0 / (font == null ? 1 : font.getDivider()); + double divider = font == null ? 1 : font.getDivider(); + double rat = textHeight / 1024.0 / divider; Color textColor2 = new Color(textColor, true); for (GLYPHENTRY entry : rec.glyphEntries) { @@ -433,9 +435,18 @@ public abstract class TextTag extends CharacterTag implements DrawableTag { Matrix matTr = Matrix.getTranslateInstance(x, y); mat = mat.concatenate(matTr); mat = mat.concatenate(Matrix.getScaleInstance(rat)); + SHAPE shape = null; if (entry.glyphIndex != -1 && glyphs != null) { // shapeNum: 1 - SHAPE shape = glyphs.get(entry.glyphIndex); + shape = glyphs.get(entry.glyphIndex); + } else if (entry instanceof DynamicTextGlyphEntry) { + DynamicTextGlyphEntry dynamicEntry = (DynamicTextGlyphEntry) entry; + if (dynamicEntry.fontFace != null) { + shape = SHAPERECORD.fontCharacterToSHAPE(new Font(dynamicEntry.fontFace, dynamicEntry.fontStyle, 12), (int) Math.round(divider * 1024), dynamicEntry.character); + } + } + + if (shape != null) { BitmapExporter.export(swf, shape, textColor2, image, mat, colorTransform); if (SHAPERECORD.DRAW_BOUNDING_BOX) { RGB borderColor = new RGBA(Color.black); @@ -444,8 +455,9 @@ public abstract class TextTag extends CharacterTag implements DrawableTag { mat = Matrix.getTranslateInstance(bounds.Xmin, bounds.Ymin).preConcatenate(mat); TextTag.drawBorder(swf, image, borderColor, fillColor, bounds, new MATRIX(), mat, colorTransform); } - x += entry.glyphAdvance; } + + x += entry.glyphAdvance; } } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/TextStyle.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/TextStyle.java index ae96988a2..781ae255b 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/TextStyle.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/TextStyle.java @@ -1,18 +1,19 @@ /* * Copyright (C) 2010-2015 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. */ + * License along with this library. + */ package com.jpexs.decompiler.flash.tags.dynamictext; import com.jpexs.decompiler.flash.tags.base.FontTag; @@ -27,6 +28,8 @@ public final class TextStyle implements Cloneable { public FontTag font; + public String fontFace; + public int fontHeight; public int fontLeading; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/DynamicTextGlyphEntry.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/DynamicTextGlyphEntry.java new file mode 100644 index 000000000..5720d3ce2 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/DynamicTextGlyphEntry.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2010-2015 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.types; + +/** + * + * @author JPEXS + */ +public class DynamicTextGlyphEntry extends GLYPHENTRY { + + public String fontFace; + + public int fontStyle; + + public char character; +}