/*
* This file is part of the Haven & Hearth game client.
* Copyright (C) 2009 Fredrik Tolf <fredrik@dolda2000.com>, and
* Björn Johannessen <johannessen.bjorn@gmail.com>
*
* Redistribution and/or modification of this file is subject to the
* terms of the GNU Lesser General Public License, version 3, as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* Other parts of this source tree adhere to other copying
* rights. Please see the file `COPYING' in the root directory of the
* source tree for details.
*
* A copy the GNU Lesser General Public License is distributed along
* with the source tree of which this file is a part in the file
* `doc/LPGL-3'. If it is missing for any reason, please see the Free
* Software Foundation's website at <http://www.fsf.org/>, or write
* to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
* Boston, MA 02111-1307 USA
*/
package haven;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.font.LineMetrics;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.font.TextMeasurer;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.text.AttributedCharacterIterator;
import java.text.AttributedCharacterIterator.Attribute;
import java.text.AttributedString;
import java.text.CharacterIterator;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class RichText extends Text {
public static final Parser std;
public static final Foundry stdf;
public List<Part> parts;
static {
Map<Attribute, Object> a = new HashMap<Attribute, Object>();
a.put(TextAttribute.FAMILY, "SansSerif");
a.put(TextAttribute.SIZE, 10);
std = new Parser(a);
stdf = new Foundry(std);
}
public static class ActionAttribute extends Attribute
{
public static final ActionAttribute ACTION = new ActionAttribute();
public ActionAttribute() {
super("action attribute");
}
}
private RichText(String text) {
super(text);
}
public String actionat(Coord c) {
for (Part part : parts) {
if(c.isect(new Coord(part.x, part.y), new Coord(part.width(), part.height()))) {
String action = null;
if(part instanceof TextPart)
{
TextPart tp = (TextPart) part;
action = tp.getAction(tp.charAt(c.x - part.x));
}
return action;
}
}
return null;
}
private static class RState {
FontRenderContext frc;
RState(FontRenderContext frc) {
this.frc = frc;
}
}
private static class PState {
PeekReader in;
PState(PeekReader in) {
this.in = in;
}
}
public static class FormatException extends RuntimeException {
public FormatException(String msg) {
super(msg);
}
}
public static class Part {
public Part next = null;
public int x, y;
public RState rs;
public void append(Part p) {
if(next == null)
next = p;
else
next.append(p);
}
public void prepare(RState rs) {
this.rs = rs;
if(next != null)
next.prepare(rs);
}
public int width() {return(0);}
public int height() {return(0);}
public int baseline() {return(0);}
public void render(Graphics2D g) {}
public Part split(int w) {
return(null);
}
}
public static class Image extends Part {
public BufferedImage img;
public Image(BufferedImage img) {
this.img = img;
}
public Image(Resource res, int id) {
res.loadwait();
for(Resource.Image img : res.layers(Resource.imgc)) {
if(img.id == id) {
this.img = img.img;
break;
}
}
if(this.img == null)
throw(new RuntimeException("Found no image with id " + id + " in " + res.toString()));
}
public int width() {return(img.getWidth());}
public int height() {return(img.getHeight());}
public int baseline() {return(img.getHeight() - 1);}
public void render(Graphics2D g) {
g.drawImage(img, x, y, null);
}
}
public static class Newline extends Part {
private Map<? extends Attribute, ?> attrs;
private LineMetrics lm;
public Newline(Map<? extends Attribute, ?> attrs) {
this.attrs = attrs;
}
private LineMetrics lm() {
if(lm == null) {
Font f;
if((f = (Font)attrs.get(TextAttribute.FONT)) != null) {
} else {
f = new Font(attrs);
}
lm = f.getLineMetrics("", rs.frc);
}
return(lm);
}
public int height() {
return((int)lm().getHeight());
}
public int baseline() {
return((int)lm().getAscent());
}
}
public static class TextPart extends Part {
public AttributedString str;
public int start, end;
private TextMeasurer tm = null;
private TextLayout tl = null;
public TextPart(AttributedString str, int start, int end) {
this.str = str;
this.start = start;
this.end = end;
}
public TextPart(String str, Map<? extends Attribute, ?> attrs) {
this((str.length() == 0)?(new AttributedString(str)):(new AttributedString(str, attrs)), 0, str.length());
}
public TextPart(String str) {
this(new AttributedString(str), 0, str.length());
}
public String getAction() {
return getAction(0);
}
public String getAction(int index) {
AttributedCharacterIterator aci = str.getIterator();
aci.setIndex(start + index);
return (String) aci.getAttributes().get(ActionAttribute.ACTION);
}
private AttributedCharacterIterator ti() {
return(str.getIterator(null, start, end));
}
public void append(Part p) {
if(next == null) {
if(p instanceof TextPart) {
TextPart tp = (TextPart)p;
str = AttributedStringBuffer.concat(ti(), tp.ti());
end = (end - start) + (tp.end - tp.start);
start = 0;
next = p.next;
} else {
next = p;
}
} else {
next.append(p);
}
}
private TextMeasurer tm() {
if(tm == null)
tm = new TextMeasurer(str.getIterator(), rs.frc);
return(tm);
}
private TextLayout tl() {
if(tl == null)
tl = tm().getLayout(start, end);
return(tl);
}
public int width() {
if(start == end) return(0);
return((int)tm().getAdvanceBetween(start, end));
}
public int charAt(int x) {
for(int i=start+1; i<end; i++) {
if(x < tm().getAdvanceBetween(start, i))
return i - start - 1;
}
return -1;
}
public int height() {
if(start == end) return(0);
return((int)(tl().getAscent() + tl().getDescent() + tl().getLeading()));
}
public int baseline() {
if(start == end) return(0);
return((int)tl().getAscent());
}
private Part split2(int e1, int s2) {
TextPart p1 = new TextPart(str, start, e1);
TextPart p2 = new TextPart(str, s2, end);
p1.next = p2;
p2.next = next;
p1.rs = p2.rs = rs;
return(p1);
}
public Part split(int w) {
if((end-start)<=1){
return null;
}
int l = start, r = end;
while(true) {
int t = l + ((r - l) / 2);
int tw;
if(t == l)
tw = 0;
else
tw = (int)tm().getAdvanceBetween(start, t);
if(tw > w) {
r = t;
} else {
l = t;
}
if(l >= r - 1)
break;
}
CharacterIterator it = str.getIterator();
for(int i = l; i >= start; i--) {
if(Character.isWhitespace(it.setIndex(i))) {
return(split2(i, i + 1));
}
}
if(l == start){
l+=1;
}
return(split2(l, l));
}
public void render(Graphics2D g) {
if(start == end) return;
tl().draw(g, x, y + tl().getAscent());
}
}
private static Map<? extends Attribute, ?> fillattrs(Object... attrs) {
Map<Attribute, Object> a = new HashMap<Attribute, Object>(std.defattrs);
for(int i = 0; i < attrs.length; i += 2)
a.put((Attribute)attrs[i], attrs[i + 1]);
return(a);
}
/*
* This fix exists for Java 1.5. Apparently, before Java 1.6,
* TextAttribute.SIZE had to be specified with a Float, and not a
* general Number; however, Java 1.6 fails to mention that in the
* documentation (which rather explicitly says that any
* java.lang.Number is perfectly OK and accepted
* practice). However, specifying ints for font size looks nicer
* in the rest of the code, so this function gets to collect all
* the ugliness of conversion in itself.
*/
private static Map<? extends Attribute, ?> fixattrs(Map<? extends Attribute, ?> attrs) {
Map<Attribute, Object> ret = new HashMap<Attribute, Object>();
for(Map.Entry<? extends Attribute, ?> e : attrs.entrySet()) {
if(e.getKey() == TextAttribute.SIZE) {
ret.put(e.getKey(), ((Number)e.getValue()).floatValue());
} else {
ret.put(e.getKey(), e.getValue());
}
}
return(ret);
}
public static class Parser {
private final Map<? extends Attribute, ?> defattrs;
public Parser(Map<? extends Attribute, ?> defattrs) {
this.defattrs = fixattrs(defattrs);
}
public Parser(Object... attrs) {
this(fillattrs(attrs));
}
private static boolean namechar(char c) {
return((c == ':') || (c == '_') || (c == '$') || (c == '.') || (c == '-') || ((c >= '0') && (c <= '9')) || ((c >= 'A') && (c <= 'Z')) || ((c >= 'a') && (c <= 'z')));
}
private static String name(PeekReader in) throws IOException {
StringBuilder buf = new StringBuilder();
while(true) {
int c = in.peek();
if(c < 0) {
break;
} else if(namechar((char)c)) {
buf.append((char)in.read());
} else {
break;
}
}
if(buf.length() == 0)
throw(new FormatException("Expected name, got `" + (char)in.peek() + "'"));
return(buf.toString());
}
private static Color a2col(String[] args) {
int r = Integer.parseInt(args[0]);
int g = Integer.parseInt(args[1]);
int b = Integer.parseInt(args[2]);
int a = 255;
if(args.length > 3)
a = Integer.parseInt(args[3]);
return(new Color(r, g, b, a));
}
private static Part tag(PState s, Map<? extends Attribute, ?> attrs) throws IOException {
s.in.peek(true);
String tn = name(s.in).intern();
String[] args;
if(s.in.peek(true) == '[') {
s.in.read();
StringBuilder buf = new StringBuilder();
while(true) {
int c = s.in.peek();
if(c < 0) {
throw(new FormatException("Unexpected end-of-input when reading tag arguments"));
} else if(c == ']') {
s.in.read();
break;
} else {
buf.append((char)s.in.read());
}
}
args = buf.toString().split(",");
} else {
args = new String[0];
}
if(tn == "img") {
Resource res = Resource.load(args[0]);
int id = -1;
if(args.length > 1)
id = Integer.parseInt(args[1]);
return(new Image(res, id));
} else {
Map<Attribute, Object> na = new HashMap<Attribute, Object>(attrs);
if(tn == "font") {
na.put(TextAttribute.FAMILY, args[0]);
if(args.length > 1)
na.put(TextAttribute.SIZE, Float.parseFloat(args[1]));
} else if(tn == "size") {
na.put(TextAttribute.SIZE, Float.parseFloat(args[0]));
} else if(tn == "b") {
na.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
} else if(tn == "i") {
na.put(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);
} else if(tn == "u") {
na.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
} else if(tn == "col") {
na.put(TextAttribute.FOREGROUND, a2col(args));
} else if(tn == "bg") {
na.put(TextAttribute.BACKGROUND, a2col(args));
} else if(tn == "a") {
na.put(ActionAttribute.ACTION, args[0]);
}
if(s.in.peek(true) != '{')
throw(new FormatException("Expected `{', got `" + (char)s.in.peek() + "'"));
s.in.read();
return(text(s, na));
}
}
private static Part text(PState s, Map<? extends Attribute, ?> attrs) throws IOException {
Part buf = new TextPart("");
StringBuilder tbuf = new StringBuilder();
while(true) {
int c = s.in.read();
if(c < 0) {
buf.append(new TextPart(tbuf.toString(), attrs));
break;
} else if(c == '\n') {
buf.append(new TextPart(tbuf.toString(), attrs));
tbuf = new StringBuilder();
buf.append(new Newline(attrs));
} else if(c == '}') {
buf.append(new TextPart(tbuf.toString(), attrs));
break;
} else if(c == '$') {
c = s.in.peek();
if((c == '$') || (c == '{') || (c == '}')) {
s.in.read();
tbuf.append((char)c);
} else {
buf.append(new TextPart(tbuf.toString(), attrs));
tbuf = new StringBuilder();
buf.append(tag(s, attrs));
}
} else {
tbuf.append((char)c);
}
}
return(buf);
}
private static Part parse(PState s, Map<? extends Attribute, ?> attrs) throws IOException {
Part res = text(s, attrs);
if(s.in.peek() >= 0)
throw(new FormatException("Junk left after the end of input: " + (char)s.in.peek()));
return(res);
}
public Part parse(Reader in, Map<? extends Attribute, ?> extra) throws IOException {
PState s = new PState(new PeekReader(in));
if(extra != null) {
Map<Attribute, Object> attrs = new HashMap<Attribute, Object>();
attrs.putAll(defattrs);
attrs.putAll(extra);
return(parse(s, attrs));
} else {
return(parse(s, defattrs));
}
}
public Part parse(Reader in) throws IOException {
return(parse(in, null));
}
public Part parse(String text, Map<? extends Attribute, ?> extra) {
try {
return(parse(new StringReader(text), extra));
} catch(IOException e) {
throw(new Error(e));
}
}
public Part parse(String text) {
return(parse(text, null));
}
public static String quote(String in) {
StringBuilder buf = new StringBuilder();
for(int i = 0; i < in.length(); i++) {
char c = in.charAt(i);
if((c == '$') || (c == '{') || (c == '}')) {
buf.append('$');
buf.append(c);
} else {
buf.append(c);
}
}
return(buf.toString());
}
}
public static class Foundry {
private Parser parser;
private RState rs;
public boolean aa = false;
private Foundry(Parser parser) {
this.parser = parser;
BufferedImage junk = TexI.mkbuf(new Coord(10, 10));
Graphics2D g = junk.createGraphics();
rs = new RState(g.getFontRenderContext());
}
public Foundry(Map<? extends Attribute, ?> defattrs) {
this(new Parser(defattrs));
}
public Foundry(Object... attrs) {
this(new Parser(attrs));
}
private static Map<? extends Attribute, ?> xlate(Font f, Color defcol) {
Map<Attribute, Object> attrs = new HashMap<Attribute, Object>();
attrs.put(TextAttribute.FONT, f);
attrs.put(TextAttribute.FOREGROUND, defcol);
return(attrs);
}
public Foundry(Font f, Color defcol) {
this(xlate(f, defcol));
}
private static void aline/* Hurrhurr, pun intended*/(List<Part> line, int y) {
int mb = 0;
for(Part p : line) {
int cb = p.baseline();
if(cb > mb) mb = cb;
}
for(Part p : line) {
p.y = y + mb - p.baseline();
}
}
private static List<Part> layout(Part fp, int w) {
List<Part> ret = new LinkedList<Part>();
List<Part> line = new LinkedList<Part>();
int x = 0, y = 0;
int mw = 0, lh = 0;
for(Part p = fp; p != null; p = p.next) {
boolean lb = p instanceof Newline;
int pw, ph;
while(true) {
p.x = x;
pw = p.width();
ph = p.height();
if(w > 0) {
if(p.x + pw > w) {
lb = true;
Part tmp = p.split(w - x);
if(tmp != null){
p = tmp;
continue;
} else {
break;
}
}
}
break;
}
ret.add(p);
line.add(p);
if(ph > lh) lh = ph;
x += pw;
if(x > mw) mw = x;
if(lb) {
aline(line, y);
x = 0;
y += lh;
lh = 0;
line = new LinkedList<Part>();
}
}
aline(line, y);
return(ret);
}
private static Coord bounds(Collection<Part> parts) {
Coord sz = new Coord(0, 0);
for(Part p : parts) {
int x = p.x + p.width();
int y = p.y + p.height();
if(x > sz.x) sz.x = x;
if(y > sz.y) sz.y = y;
}
return(sz);
}
public RichText render(String text, int width, Object... extra) {
RichText rt = new RichText(text);
Map<? extends Attribute, ?> extram = null;
if(extra.length > 0) {
extram = fillattrs(extra);
}
Part fp = parser.parse(text, extram);
fp.prepare(rs);
rt.parts = layout(fp, width);
Coord sz = bounds(rt.parts);
if((width>0)&&(sz.x > width)) sz.x = width;
if(sz.x < 1) sz = sz.add(1, 0);
if(sz.y < 1) sz = sz.add(0, 1);
BufferedImage img = TexI.mkbuf(sz);
Graphics2D g = img.createGraphics();
if(aa)
Utils.AA(g);
rt.img = img;
for(Part p : rt.parts)
p.render(g);
return(rt);
}
public RichText render(String text) {
return(render(text, 0));
}
}
public static RichText render(String text, int width, Object... extra) {
return(stdf.render(text, width, extra));
}
public static void main(String[] args) throws Exception {
String cmd = args[0].intern();
if(cmd == "render") {
Map<Attribute, Object> a = new HashMap<Attribute, Object>(std.defattrs);
PosixArgs opt = PosixArgs.getopt(args, 1, "aw:f:s:");
boolean aa = false;
int width = 0;
for(char c : opt.parsed()) {
if(c == 'a') {
aa = true;
} else if(c == 'f') {
a.put(TextAttribute.FAMILY, opt.arg);
} else if(c == 'w') {
width = Integer.parseInt(opt.arg);
} else if(c == 's') {
a.put(TextAttribute.SIZE, Integer.parseInt(opt.arg));
}
}
Foundry fnd = new Foundry(a);
fnd.aa = aa;
RichText t = fnd.render(opt.rest[0], width);
java.io.OutputStream out = new java.io.FileOutputStream(opt.rest[1]);
javax.imageio.ImageIO.write(t.img, "PNG", out);
out.close();
} else if(cmd == "pagina") {
PosixArgs opt = PosixArgs.getopt(args, 1, "aw:");
boolean aa = false;
int width = 0;
for(char c : opt.parsed()) {
if(c == 'a') {
aa = true;
} else if(c == 'w') {
width = Integer.parseInt(opt.arg);
}
}
Foundry fnd = new Foundry();
fnd.aa = aa;
Resource res = Resource.load(opt.rest[0]);
res.loadwaitint();
Resource.Pagina p = res.layer(Resource.pagina);
if(p == null)
throw(new Exception("No pagina in " + res + ", loaded from " + res.source));
RichText t = fnd.render(p.text, width);
java.io.OutputStream out = new java.io.FileOutputStream(opt.rest[1]);
javax.imageio.ImageIO.write(t.img, "PNG", out);
out.close();
}
}
}