/*
* 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.base;
import com.jpexs.decompiler.flash.AppResources;
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.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.SWFType;
import com.jpexs.helpers.ByteArrayRange;
import com.jpexs.helpers.Helper;
import com.jpexs.helpers.SerializableImage;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
* @author JPEXS
*/
public abstract class StaticTextTag extends TextTag {
@SWFType(BasicType.UI16)
public int characterID;
protected int glyphBits;
protected int advanceBits;
public RECT textBounds;
public MATRIX textMatrix;
public List<TEXTRECORD> textRecords;
public abstract int getTextNum();
public StaticTextTag(SWF swf, int id, String name, ByteArrayRange data) {
super(swf, id, name, data);
}
@Override
public final void readData(SWFInputStream sis, ByteArrayRange data, int level, boolean parallel, boolean skipUnusualTags, boolean lazy) throws IOException {
characterID = sis.readUI16("characterID");
textBounds = sis.readRECT("textBounds");
textMatrix = sis.readMatrix("textMatrix");
glyphBits = sis.readUI8("glyphBits");
advanceBits = sis.readUI8("advanceBits");
textRecords = new ArrayList<>();
TEXTRECORD tr;
while ((tr = sis.readTEXTRECORD(getTextNum(), glyphBits, advanceBits, "record")) != null) {
textRecords.add(tr);
}
}
/**
* 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(textBounds);
sos.writeMatrix(textMatrix);
int glyphBits = 0;
int advanceBits = 0;
for (TEXTRECORD tr : textRecords) {
for (GLYPHENTRY ge : tr.glyphEntries) {
glyphBits = SWFOutputStream.enlargeBitCountU(glyphBits, ge.glyphIndex);
advanceBits = SWFOutputStream.enlargeBitCountS(advanceBits, ge.glyphAdvance);
}
}
if (Configuration._debugCopy.get()) {
glyphBits = Math.max(glyphBits, this.glyphBits);
advanceBits = Math.max(advanceBits, this.advanceBits);
}
sos.writeUI8(glyphBits);
sos.writeUI8(advanceBits);
for (TEXTRECORD tr : textRecords) {
sos.writeTEXTRECORD(tr, getTextNum(), glyphBits, advanceBits);
}
sos.writeUI8(0);
}
@Override
public RECT getBounds() {
return textBounds;
}
@Override
public MATRIX getTextMatrix() {
return textMatrix;
}
@Override
public void setBounds(RECT r) {
textBounds = r;
}
@Override
public List<String> getTexts() {
FontTag fnt = null;
List<String> ret = new ArrayList<>();
for (TEXTRECORD rec : textRecords) {
if (rec.styleFlagsHasFont) {
FontTag fnt2 = swf.getFont(rec.fontId);
if (fnt2 != null) {
fnt = fnt2;
}
}
if (rec.styleFlagsHasXOffset || rec.styleFlagsHasYOffset) {
/*if (!ret.isEmpty()) {
ret += "\r\n";
}*/
}
if (fnt == null) {
ret.add(AppResources.translate("fontNotFound").replace("%fontId%", Integer.toString(rec.fontId)));
} else {
ret.add(rec.getText(fnt));
}
}
return ret;
}
@Override
public List<Integer> getFontIds() {
List<Integer> ret = new ArrayList<>();
for (TEXTRECORD rec : textRecords) {
if (rec.styleFlagsHasFont) {
ret.add(rec.fontId);
}
}
return ret;
}
@Override
public void updateTextBounds() {
updateTextBounds(textBounds);
}
@Override
public boolean alignText(TextAlign textAlign) {
alignText(swf, textRecords, textAlign);
setModified(true);
return true;
}
@Override
public boolean translateText(int diff) {
textMatrix.translateX += diff;
updateTextBounds();
setModified(true);
return true;
}
@Override
public RECT getRect(Set<BoundedTag> added) {
return textBounds;
}
@Override
public ExportRectangle calculateTextBounds() {
return calculateTextBounds(swf, textRecords, getTextMatrix());
}
@Override
public int getNumFrames() {
return 1;
}
@Override
public boolean isSingleFrame() {
return true;
}
@Override
public int getCharacterId() {
return characterID;
}
@Override
public void setCharacterId(int characterId) {
this.characterID = characterId;
}
@Override
public HighlightedText getFormattedText(boolean ignoreLetterSpacing) {
FontTag fnt = null;
HighlightedTextWriter writer = new HighlightedTextWriter(Configuration.getCodeFormatting(), true);
writer.append("[").newLine();
writer.append("xmin ").append(textBounds.Xmin).newLine();
writer.append("ymin ").append(textBounds.Ymin).newLine();
writer.append("xmax ").append(textBounds.Xmax).newLine();
writer.append("ymax ").append(textBounds.Ymax).newLine();
if (textMatrix.translateX != 0) {
writer.append("translatex ").append(textMatrix.translateX).newLine();
}
if (textMatrix.translateY != 0) {
writer.append("translatey ").append(textMatrix.translateY).newLine();
}
if (textMatrix.hasScale) {
writer.append("scalex ").append(textMatrix.scaleX).newLine();
writer.append("scaley ").append(textMatrix.scaleY).newLine();
}
if (textMatrix.hasRotate) {
writer.append("rotateskew0 ").append(textMatrix.rotateSkew0).newLine();
writer.append("rotateskew1 ").append(textMatrix.rotateSkew1).newLine();
}
writer.append("]");
int textHeight = 12;
for (TEXTRECORD rec : textRecords) {
if (rec.styleFlagsHasFont || rec.styleFlagsHasColor || rec.styleFlagsHasXOffset || rec.styleFlagsHasYOffset) {
writer.append("[").newLine();
if (rec.styleFlagsHasFont) {
FontTag fnt2 = swf.getFont(rec.fontId);
if (fnt2 != null) {
fnt = fnt2;
}
writer.append("font ").append(rec.fontId).newLine();
writer.append("height ").append(rec.textHeight).newLine();
textHeight = rec.textHeight;
}
if (fnt != null && !ignoreLetterSpacing) {
int letterSpacing = detectLetterSpacing(rec, fnt, textHeight);
if (letterSpacing != 0) {
writer.append("letterspacing ").append(letterSpacing).newLine();
}
}
if (rec.styleFlagsHasColor) {
if (getTextNum() == 1) {
writer.append("color ").append(rec.textColor.toHexRGB()).newLine();
} else {
writer.append("color ").append(rec.textColorA.toHexARGB()).newLine();
}
}
if (rec.styleFlagsHasXOffset) {
writer.append("x ").append(rec.xOffset).newLine();
}
if (rec.styleFlagsHasYOffset) {
writer.append("y ").append(rec.yOffset).newLine();
}
writer.append("]");
}
if (fnt == null) {
writer.append(AppResources.translate("fontNotFound").replace("%fontId%", Integer.toString(rec.fontId)));
} else {
writer.hilightSpecial(Helper.escapeActionScriptString(rec.getText(fnt)).replace("[", "\\[").replace("]", "\\]"), 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;
List<TEXTRECORD> textRecords = new ArrayList<>();
RGB color = null;
RGBA colorA = null;
int fontId = -1;
int textHeight = -1;
int letterSpacing = 0;
FontTag font = null;
Integer x = null;
Integer y = null;
int currentX = 0;
int currentY = 0;
int maxX = Integer.MIN_VALUE;
int minX = Integer.MAX_VALUE;
MATRIX textMatrix = new MATRIX();
textMatrix.hasRotate = false;
textMatrix.hasScale = false;
RECT textBounds = new RECT();
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 "color":
if (getTextNum() == 1) {
Matcher m = Pattern.compile("#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])").matcher(paramValue);
if (m.matches()) {
color = new RGB(Integer.parseInt(m.group(1), 16), Integer.parseInt(m.group(2), 16), Integer.parseInt(m.group(3), 16));
} else {
throw new TextParseException("Invalid color. Valid format is #rrggbb. Found: " + paramValue, lexer.yyline());
}
} else {
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()) {
colorA = 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 "font":
try {
fontId = Integer.parseInt(paramValue);
FontTag ft = swf.getFont(fontId);
if (ft == null) {
throw new TextParseException("Font not found.", lexer.yyline());
}
font = (FontTag) ft;
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid font id - number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "height":
try {
textHeight = Integer.parseInt(paramValue);
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid font height - number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "letterspacing":
try {
letterSpacing = Integer.parseInt(paramValue);
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid font letter spacing - number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "x":
try {
x = Integer.parseInt(paramValue);
currentX = x;
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid x position - number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "y":
try {
y = Integer.parseInt(paramValue);
currentY = y;
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid y position - number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "xmin":
try {
textBounds.Xmin = Integer.parseInt(paramValue);
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid xmin position - number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "xmax":
try {
textBounds.Xmax = Integer.parseInt(paramValue);
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid xmax position - number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "ymin":
try {
textBounds.Ymin = Integer.parseInt(paramValue);
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid ymin position - number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "ymax":
try {
textBounds.Ymax = Integer.parseInt(paramValue);
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid ymax position - number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "scalex":
try {
textMatrix.scaleX = Integer.parseInt(paramValue);
textMatrix.hasScale = true;
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid scalex value - number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "scaley":
try {
textMatrix.scaleY = Integer.parseInt(paramValue);
textMatrix.hasScale = true;
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid scalex value - number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "rotateskew0":
try {
textMatrix.rotateSkew0 = Integer.parseInt(paramValue);
textMatrix.hasRotate = true;
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid rotateskew0 value - number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "rotateskew1":
try {
textMatrix.rotateSkew1 = Integer.parseInt(paramValue);
textMatrix.hasRotate = true;
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid rotateskew1 value - number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "translatex":
try {
textMatrix.translateX = Integer.parseInt(paramValue);
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid translatex value - number expected. Found: " + paramValue, lexer.yyline());
}
break;
case "translatey":
try {
textMatrix.translateY = Integer.parseInt(paramValue);
} catch (NumberFormatException nfe) {
throw new TextParseException("Invalid translatey value - number expected. Found: " + paramValue, lexer.yyline());
}
break;
default:
throw new TextParseException("Unrecognized parameter name: " + paramName, lexer.yyline());
}
break;
case TEXT:
String txt = (texts == null || textIdx >= texts.length) ? (String) s.values[0] : texts[textIdx++];
if (txt == null || (font == null && txt.isEmpty())) {
continue;
}
if (font == null) {
throw new TextParseException("Font not defined", lexer.yyline());
}
while (txt.charAt(0) == '\r' || txt.charAt(0) == '\n') {
txt = txt.substring(1);
}
while (txt.charAt(txt.length() - 1) == '\r' || txt.charAt(txt.length() - 1) == '\n') {
txt = txt.substring(0, txt.length() - 1);
}
StringBuilder txtSb = new StringBuilder();
for (int i = 0; i < txt.length(); i++) {
char c = txt.charAt(i);
if (!font.containsChar(c)) {
if (!missingCharHandler.handle(this, font, c)) {
if (!missingCharHandler.getIgnoreMissingCharacters()) {
return false;
}
} else {
return setFormattedText(missingCharHandler, formattedText, texts);
}
} else {
txtSb.append(c);
}
}
txt = txtSb.toString();
TEXTRECORD tr = new TEXTRECORD();
textRecords.add(tr);
if (fontId > -1) {
tr.fontId = fontId;
tr.textHeight = textHeight;
fontId = -1;
tr.styleFlagsHasFont = true;
}
if (getTextNum() == 1) {
if (color != null) {
tr.textColor = color;
tr.styleFlagsHasColor = true;
color = null;
}
} else if (colorA != null) {
tr.textColorA = colorA;
tr.styleFlagsHasColor = true;
colorA = null;
}
if (x != null) {
tr.xOffset = x;
tr.styleFlagsHasXOffset = true;
x = null;
}
if (y != null) {
tr.yOffset = y;
tr.styleFlagsHasYOffset = true;
y = null;
}
tr.glyphEntries = new ArrayList<>(txt.length());
for (int i = 0; i < txt.length(); i++) {
char c = txt.charAt(i);
Character nextChar = null;
if (i + 1 < txt.length()) {
nextChar = txt.charAt(i + 1);
}
GLYPHENTRY ge = new GLYPHENTRY();
ge.glyphIndex = font.charToGlyph(c);
int advance = getAdvance(font, ge.glyphIndex, textHeight, c, nextChar) + letterSpacing;
ge.glyphAdvance = advance;
tr.glyphEntries.add(ge);
currentX += advance;
}
if (currentX > maxX) {
maxX = currentX;
}
if (currentX < minX) {
minX = currentX;
}
break;
}
}
setModified(true);
this.textRecords = textRecords;
this.textMatrix = textMatrix;
this.textBounds = textBounds;
} catch (IOException ex) {
return false;
} catch (TextParseException ex) {
throw ex;
}
updateTextBounds();
return true;
}
private int getAdvance(FontTag font, int glyphIndex, int textHeight, char c, Character nextChar) {
int advance;
if (font.hasLayout()) {
int kerningAdjustment = 0;
if (nextChar != null) {
kerningAdjustment = font.getCharKerningAdjustment(c, nextChar);
}
advance = (int) Math.round(((double) textHeight * (font.getGlyphAdvance(glyphIndex) + kerningAdjustment)) / (font.getDivider() * 1024.0));
} else {
String fontName = font.getSystemFontName();
advance = (int) Math.round(SWF.unitDivisor * FontTag.getSystemFontAdvance(fontName, font.getFontStyle(), (int) (textHeight / SWF.unitDivisor), c, nextChar));
}
return advance;
}
private int detectLetterSpacing(TEXTRECORD textRecord, FontTag font, int textHeight) {
int totalLetterSpacing = 0;
List<GLYPHENTRY> glyphEntries = textRecord.glyphEntries;
for (int i = 0; i < glyphEntries.size(); i++) {
GLYPHENTRY glyph = glyphEntries.get(i);
GLYPHENTRY nextGlyph = null;
if (i + 1 < glyphEntries.size()) {
nextGlyph = glyphEntries.get(i + 1);
}
char c = font.glyphToChar(glyph.glyphIndex);
Character nextChar = nextGlyph == null ? null : font.glyphToChar(nextGlyph.glyphIndex);
int advance = getAdvance(font, glyph.glyphIndex, textHeight, c, nextChar);
int letterSpacing = glyph.glyphAdvance - advance;
totalLetterSpacing += letterSpacing;
}
return (int) Math.round(totalLetterSpacing / glyphEntries.size());
}
@Override
public void getNeededCharacters(Set<Integer> needed) {
for (TEXTRECORD tr : textRecords) {
if (tr.styleFlagsHasFont) {
needed.add(tr.fontId);
}
}
}
@Override
public boolean replaceCharacter(int oldCharacterId, int newCharacterId) {
boolean modified = false;
for (TEXTRECORD tr : textRecords) {
if (tr.fontId == oldCharacterId) {
tr.fontId = newCharacterId;
modified = true;
}
}
if (modified) {
setModified(true);
}
return modified;
}
@Override
public boolean removeCharacter(int characterId) {
boolean modified = false;
for (TEXTRECORD tr : textRecords) {
if (tr.fontId == characterId) {
tr.styleFlagsHasFont = false;
tr.fontId = 0;
modified = true;
}
}
if (modified) {
setModified(true);
}
return modified;
}
@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) {
staticTextToImage(swf, textRecords, getTextNum(), image, textMatrix, transformation, colorTransform);
/*try {
TextTag originalTag = (TextTag) getOriginalTag();
if (isModified()) {
originalTag.toImage(frame, time, ratio, renderContext, image, transformation, new ConstantColorColorTransform(0xFFC0C0C0));
}
staticTextToImage(swf, textRecords, getTextNum(), image, getTextMatrix(), transformation, new ConstantColorColorTransform(0xFF000000));
} catch (InterruptedException | IOException ex) {
Logger.getLogger(TextTag.class.getName()).log(Level.SEVERE, null, ex);
}*/
}
@Override
public void toSVG(SVGExporter exporter, int ratio, ColorTransform colorTransform, int level) {
staticTextToSVG(swf, textRecords, getTextNum(), exporter, getRect(), textMatrix, colorTransform, 1);
}
@Override
public void toHtmlCanvas(StringBuilder result, double unitDivisor) {
staticTextToHtmlCanvas(unitDivisor, swf, textRecords, getTextNum(), result, textBounds, textMatrix, null);
}
}