UFontFace.java

/* ========================================================================
 * PlantUML : a free UML diagram generator
 * ========================================================================
 *
 * (C) Copyright 2009-2024, Arnaud Roques
 *
 * Project Info:  https://plantuml.com
 * 
 * If you like this project or if you find it useful, you can support us at:
 * 
 * https://plantuml.com/patreon (only 1$ per month!)
 * https://plantuml.com/paypal
 * 
 * This file is part of PlantUML.
 *
 * PlantUML is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PlantUML distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
 * License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 *
 * Original Author:  Arnaud Roques
 * 
 *
 */
package net.sourceforge.plantuml.klimt.font;

import java.awt.Font;
import java.awt.font.TextAttribute;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * Immutable value object that combines a font style (normal/italic) and a font
 * weight (CSS 100-900). This mirrors the CSS model where {@code font-style} and
 * {@code font-weight} are independent axes.
 * <p>
 * A {@code UFontFace} can be applied to any {@link java.awt.Font} via
 * {@link #deriveFont(Font)} to produce a font with the requested style and
 * weight.
 */
public class UFontFace {

	private static final UFontFace NORMAL = new UFontFace(UFontStyle.NORMAL, UFontWeight.fromCssValue(400));
	private static final UFontFace BOLD = new UFontFace(UFontStyle.NORMAL, UFontWeight.fromCssValue(700));
	private static final UFontFace ITALIC = new UFontFace(UFontStyle.ITALIC, UFontWeight.fromCssValue(400));
	private static final UFontFace BOLD_ITALIC = new UFontFace(UFontStyle.ITALIC, UFontWeight.fromCssValue(700));

	private final UFontStyle style;
	private final UFontWeight weight;

	private UFontFace(UFontStyle style, UFontWeight weight) {
		this.style = Objects.requireNonNull(style);
		this.weight = Objects.requireNonNull(weight);
	}

	/**
	 * Returns the standard normal face (weight 400, non-italic).
	 */
	public static UFontFace normal() {
		return NORMAL;
	}

	/**
	 * Returns the standard bold face (weight 700, non-italic).
	 */
	public static UFontFace bold() {
		return BOLD;
	}

	/**
	 * Returns the standard italic face (weight 400, italic).
	 */
	public static UFontFace italic() {
		return ITALIC;
	}

	/**
	 * Returns the standard bold-italic face (weight 700, italic).
	 */
	public static UFontFace boldItalic() {
		return BOLD_ITALIC;
	}

	/**
	 * Creates a face from an explicit style and weight.
	 */
	public static UFontFace of(UFontStyle style, UFontWeight weight) {
		return new UFontFace(style, weight);
	}

	/**
	 * Creates a face from a CSS numeric weight (100-900) and a style.
	 */
	public static UFontFace of(UFontStyle style, int cssWeight) {
		return new UFontFace(style, UFontWeight.fromCssValue(cssWeight));
	}

	/**
	 * Parses a CSS {@code font-weight} string. Accepts numeric values (100-900)
	 * and keywords: {@code normal}, {@code bold}, {@code lighter},
	 * {@code bolder}.
	 *
	 * @return a face with the parsed weight and NORMAL style, or {@code null} if
	 *         the input cannot be parsed
	 */
	public static UFontFace fromCssWeight(String cssValue) {
		if (cssValue == null)
			return null;

		final String trimmed = cssValue.trim().toLowerCase();
		switch (trimmed) {
		case "normal":
			return NORMAL;
		case "bold":
			return BOLD;
		case "lighter":
			return of(UFontStyle.NORMAL, 300);
		case "bolder":
			return of(UFontStyle.NORMAL, 800);
		default:
			try {
				final int w = Integer.parseInt(trimmed);
				return of(UFontStyle.NORMAL, w);
			} catch (NumberFormatException e) {
				return null;
			}
		}
	}

	public UFontStyle getStyle() {
		return style;
	}

	public UFontWeight getWeight() {
		return weight;
	}

	public int getCssWeight() {
		return weight.getWeight();
	}

	public boolean isItalic() {
		return style == UFontStyle.ITALIC;
	}

	public boolean isBold() {
		return weight.getWeight() >= 700;
	}

	/**
	 * Returns a new face with the given style, keeping the current weight.
	 */
	public UFontFace withStyle(UFontStyle newStyle) {
		if (this.style == newStyle)
			return this;
		return new UFontFace(newStyle, this.weight);
	}

	/**
	 * Returns a new face with the given CSS weight, keeping the current style.
	 */
	public UFontFace withWeight(int cssWeight) {
		final UFontWeight newWeight = UFontWeight.fromCssValue(cssWeight);
		if (this.weight.equals(newWeight))
			return this;
		return new UFontFace(this.style, newWeight);
	}

	/**
	 * Returns a new face with the given weight, keeping the current style.
	 */
	public UFontFace withWeight(UFontWeight newWeight) {
		return new UFontFace(this.style, newWeight);
	}

	// ::remove folder when __HAXE__

	/**
	 * Maps a CSS numeric weight (100-900) to the corresponding
	 * {@link TextAttribute#WEIGHT} float constant.
	 */
	public float toTextAttributeWeight() {
		final int w = weight.getWeight();
		if (w <= 200)
			return TextAttribute.WEIGHT_EXTRA_LIGHT;
		if (w <= 300)
			return TextAttribute.WEIGHT_LIGHT;
		if (w <= 400)
			return TextAttribute.WEIGHT_REGULAR;
		if (w <= 500)
			return TextAttribute.WEIGHT_MEDIUM;
		if (w <= 600)
			return TextAttribute.WEIGHT_SEMIBOLD;
		if (w <= 700)
			return TextAttribute.WEIGHT_BOLD;
		if (w <= 800)
			return TextAttribute.WEIGHT_HEAVY;
		return TextAttribute.WEIGHT_ULTRABOLD;
	}

	/**
	 * Derives a new {@link java.awt.Font} from the given base font, applying both
	 * style (italic) and weight via {@link TextAttribute}s.
	 * <p>
	 * This is the primary integration point for Java2D rendering (PNG).
	 */
	public Font deriveFont(Font baseFont) {
		final Map<TextAttribute, Object> attrs = new HashMap<>();
		attrs.put(TextAttribute.WEIGHT, toTextAttributeWeight());
		if (isItalic())
			attrs.put(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);
		return baseFont.deriveFont(attrs);
	}

	// ::done

	/**
	 * Returns a CSS-compatible {@code font-weight} string for SVG output.
	 */
	public String toCssWeightString() {
		return String.valueOf(weight.getWeight());
	}

	/**
	 * Returns a CSS-compatible {@code font-style} string for SVG output.
	 */
	public String toCssStyleString() {
		return isItalic() ? "italic" : "normal";
	}

	@Override
	public int hashCode() {
		return Objects.hash(style, weight.getWeight());
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (!(obj instanceof UFontFace))
			return false;
		final UFontFace other = (UFontFace) obj;
		return this.style == other.style && this.weight.getWeight() == other.weight.getWeight();
	}

	@Override
	public String toString() {
		return "UFontFace[" + style + ", weight=" + weight.getWeight() + "]";
	}

}