|
|
@ -1,29 +1,35 @@ |
|
|
|
package com.fine.theme.icon.svg; |
|
|
|
package com.fine.theme.icon.svg; |
|
|
|
|
|
|
|
|
|
|
|
import com.fine.theme.icon.DisabledIcon; |
|
|
|
import com.fine.theme.icon.DisabledIcon; |
|
|
|
|
|
|
|
import com.fine.theme.icon.GraphicsFilter; |
|
|
|
import com.fine.theme.icon.IconResource; |
|
|
|
import com.fine.theme.icon.IconResource; |
|
|
|
|
|
|
|
import com.fine.theme.icon.WhiteIcon; |
|
|
|
|
|
|
|
import com.formdev.flatlaf.FlatLaf; |
|
|
|
|
|
|
|
import com.formdev.flatlaf.ui.FlatUIUtils; |
|
|
|
|
|
|
|
import com.formdev.flatlaf.util.GrayFilter; |
|
|
|
import com.fr.clone.cloning.Immutable; |
|
|
|
import com.fr.clone.cloning.Immutable; |
|
|
|
import org.apache.batik.transcoder.TranscoderException; |
|
|
|
import com.fr.value.NullableLazyValue; |
|
|
|
import org.apache.batik.transcoder.TranscoderInput; |
|
|
|
import com.github.weisj.jsvg.SVGDocument; |
|
|
|
|
|
|
|
import com.github.weisj.jsvg.attributes.ViewBox; |
|
|
|
|
|
|
|
import com.github.weisj.jsvg.parser.SVGLoader; |
|
|
|
import org.jetbrains.annotations.NotNull; |
|
|
|
import org.jetbrains.annotations.NotNull; |
|
|
|
|
|
|
|
|
|
|
|
import javax.swing.Icon; |
|
|
|
import javax.swing.Icon; |
|
|
|
import java.awt.Color; |
|
|
|
import javax.swing.JComponent; |
|
|
|
|
|
|
|
import javax.swing.UIManager; |
|
|
|
import java.awt.Component; |
|
|
|
import java.awt.Component; |
|
|
|
import java.awt.Dimension; |
|
|
|
import java.awt.Dimension; |
|
|
|
import java.awt.Graphics; |
|
|
|
import java.awt.Graphics; |
|
|
|
import java.awt.Graphics2D; |
|
|
|
import java.awt.Graphics2D; |
|
|
|
import java.awt.image.BufferedImage; |
|
|
|
import java.awt.image.RGBImageFilter; |
|
|
|
|
|
|
|
import java.util.Objects; |
|
|
|
|
|
|
|
import java.util.StringJoiner; |
|
|
|
|
|
|
|
|
|
|
|
import static com.fine.theme.utils.FineUIScale.scale; |
|
|
|
import static com.fine.theme.utils.FineUIScale.scale; |
|
|
|
import static com.fine.theme.utils.FineUIScale.unscale; |
|
|
|
|
|
|
|
import static com.fine.theme.utils.FineUIUtils.RETINA_SCALE_FACTOR; |
|
|
|
|
|
|
|
import static com.fine.theme.utils.FineUIUtils.getRetina; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* svg图标 |
|
|
|
* svg图标 |
|
|
|
* 1.绘制长度会跟随DPI比率变化 |
|
|
|
* 1.绘制长度会跟随DPI比率变化 |
|
|
|
* 2.如果设备支持 Retina 则支持Retina绘制HIDPI图 |
|
|
|
|
|
|
|
* 1跟2的缩放原因不同,因此不能混淆,宽高测量等依旧 |
|
|
|
* 1跟2的缩放原因不同,因此不能混淆,宽高测量等依旧 |
|
|
|
* 使用DPI缩放进行,只有绘制内容时使用Retina绘制(如果有) |
|
|
|
* 使用DPI缩放进行,只有绘制内容时使用Retina绘制(如果有) |
|
|
|
* Retina绘制不影响最终尺寸,注意区分 |
|
|
|
* Retina绘制不影响最终尺寸,注意区分 |
|
|
@ -33,27 +39,32 @@ import static com.fine.theme.utils.FineUIUtils.getRetina; |
|
|
|
* Created on 2023/11/15 |
|
|
|
* Created on 2023/11/15 |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
@Immutable |
|
|
|
@Immutable |
|
|
|
public class SvgIcon implements DisabledIcon, Icon { |
|
|
|
public class SvgIcon implements DisabledIcon, WhiteIcon, Icon { |
|
|
|
|
|
|
|
|
|
|
|
protected transient BufferedImage cache; |
|
|
|
public enum Type { |
|
|
|
|
|
|
|
disable, white, origin |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private final Dimension size; |
|
|
|
private final Dimension size; |
|
|
|
private final IconResource resource; |
|
|
|
private final IconResource resource; |
|
|
|
private final boolean disable; |
|
|
|
private final Type type; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private final NullableLazyValue<SVGDocument> svgDocument = NullableLazyValue.createValue(() -> load(Type.origin)); |
|
|
|
|
|
|
|
private final NullableLazyValue<SVGDocument> whiteSvgDocument = NullableLazyValue.createValue(() -> load(Type.white)); |
|
|
|
|
|
|
|
|
|
|
|
public SvgIcon(IconResource resource, Dimension size) { |
|
|
|
public SvgIcon(IconResource resource, Dimension size) { |
|
|
|
this(resource, size, false); |
|
|
|
this(resource, size, Type.origin); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public SvgIcon(IconResource resource, Dimension size, boolean disable) { |
|
|
|
public SvgIcon(IconResource resource, Dimension size, Type type) { |
|
|
|
this.resource = resource; |
|
|
|
this.resource = resource; |
|
|
|
// 根据dpi进行缩放
|
|
|
|
// 根据dpi进行缩放
|
|
|
|
this.size = scale(size); |
|
|
|
this.size = scale(size); |
|
|
|
this.disable = disable; |
|
|
|
this.type = type; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public SvgIcon(IconResource resource, int side) { |
|
|
|
public SvgIcon(IconResource resource, int side) { |
|
|
|
this(resource, new Dimension(side, side), false); |
|
|
|
this(resource, new Dimension(side, side), Type.origin); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
@ -62,46 +73,21 @@ public class SvgIcon implements DisabledIcon, Icon { |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
@Override |
|
|
|
@Override |
|
|
|
public void paintIcon(Component c, Graphics g, int x, int y) { |
|
|
|
public void paintIcon(Component c, Graphics g, int x, int y) { |
|
|
|
if (isCacheValid()) { |
|
|
|
if (type == Type.disable) { |
|
|
|
if (cache != null) { |
|
|
|
g = GrayGraphics(g); |
|
|
|
cache.flush(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
Dimension scale = scaleRetina(size); |
|
|
|
|
|
|
|
if (disable && c != null) { |
|
|
|
|
|
|
|
cache = toGrayImage(scale, c.getBackground()); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
cache = toImage(scale); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (getRetina()) { |
|
|
|
|
|
|
|
// 高清绘制的原理:scale(1/2,1/2)的原理是坐标减半,底层是矩阵进行坐标变换,意思是坐标减半进行绘制,
|
|
|
|
|
|
|
|
// 这样就可以将两倍图绘制到一倍的大小,如果这时候设备支持Retina绘制(四个像素模拟一个像素),
|
|
|
|
|
|
|
|
// 正好就可以将4个像素利用起来,每个像素点都有不同的颜色,而不像之前只能是四个共用一个颜色。因此图像
|
|
|
|
|
|
|
|
// 可以更加细腻当然,绘图之后,需要将这个坐标变换给恢复。
|
|
|
|
|
|
|
|
((Graphics2D) g).scale(1.0 / RETINA_SCALE_FACTOR, 1.0 / RETINA_SCALE_FACTOR); |
|
|
|
|
|
|
|
g.drawImage(cache, x * RETINA_SCALE_FACTOR, y * RETINA_SCALE_FACTOR, null); |
|
|
|
|
|
|
|
((Graphics2D) g).scale(RETINA_SCALE_FACTOR, RETINA_SCALE_FACTOR); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
g.drawImage(cache, x, y, null); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Object[] oldRenderingHints = FlatUIUtils.setRenderingHints(g); |
|
|
|
|
|
|
|
render(c, g, x, y); |
|
|
|
|
|
|
|
FlatUIUtils.resetRenderingHints(g, oldRenderingHints); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private boolean isCacheValid() { |
|
|
|
public Graphics2D GrayGraphics(Graphics g) { |
|
|
|
return cache == null |
|
|
|
Object grayFilterObj = UIManager.get("Component.grayFilter"); |
|
|
|
|| cache.getWidth() != scaleRetina(size.width) |
|
|
|
RGBImageFilter grayFilter = (grayFilterObj instanceof RGBImageFilter) |
|
|
|
|| cache.getHeight() != scaleRetina(size.height); |
|
|
|
? (RGBImageFilter) grayFilterObj |
|
|
|
} |
|
|
|
: GrayFilter.createDisabledIconFilter(FlatLaf.isLafDark()); |
|
|
|
|
|
|
|
|
|
|
|
private static int scaleRetina(int size) { |
|
|
|
|
|
|
|
return getRetina() |
|
|
|
|
|
|
|
? size * RETINA_SCALE_FACTOR |
|
|
|
|
|
|
|
: size; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static Dimension scaleRetina(Dimension dimension) { |
|
|
|
return new GraphicsFilter((Graphics2D) g.create(), grayFilter); |
|
|
|
return getRetina() |
|
|
|
|
|
|
|
? new Dimension(dimension.width * RETINA_SCALE_FACTOR, dimension.height * RETINA_SCALE_FACTOR) |
|
|
|
|
|
|
|
: dimension; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
@Override |
|
|
@ -114,44 +100,48 @@ public class SvgIcon implements DisabledIcon, Icon { |
|
|
|
return size.height; |
|
|
|
return size.height; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
private void render(Component c, Graphics g, int x, int y) { |
|
|
|
* 根据指定尺寸绘制图片,这里尺寸为结算后的尺寸, |
|
|
|
if (type == Type.white) { |
|
|
|
* 因此不必进行缩放 |
|
|
|
Objects.requireNonNull(whiteSvgDocument.getValue()) |
|
|
|
* |
|
|
|
.render((JComponent) c, (Graphics2D) g, new ViewBox(x, y, size.width, size.height)); |
|
|
|
* @param size 图像尺寸 |
|
|
|
} else { |
|
|
|
* @return 图像 |
|
|
|
Objects.requireNonNull(svgDocument.getValue()) |
|
|
|
*/ |
|
|
|
.render((JComponent) c, (Graphics2D) g, new ViewBox(x, y, size.width, size.height)); |
|
|
|
private BufferedImage toImage(Dimension size) { |
|
|
|
} |
|
|
|
SvgTranscoder transcoder = new SvgTranscoder(size); |
|
|
|
} |
|
|
|
TranscoderInput transcoderInput = new TranscoderInput(resource.getInputStream()); |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
transcoder.transcode(transcoderInput, null); |
|
|
|
private SVGDocument load(Type type) { |
|
|
|
return transcoder.getImage(); |
|
|
|
SVGLoader loader = new SVGLoader(); |
|
|
|
} catch (TranscoderException e) { |
|
|
|
return type == Type.white |
|
|
|
throw new RuntimeException(e); |
|
|
|
? loader.load(resource.getInputStream(), new WhiteParser()) |
|
|
|
|
|
|
|
: loader.load(resource.getInputStream()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
|
|
public String toString() { |
|
|
|
|
|
|
|
return new StringJoiner(", ", SvgIcon.class.getSimpleName() + "[", "]") |
|
|
|
|
|
|
|
.add("resource=" + resource) |
|
|
|
|
|
|
|
.add("type=" + type) |
|
|
|
|
|
|
|
.add("size=" + size) |
|
|
|
|
|
|
|
.toString(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* 简单的灰化处理,不能有透明度,因此需要传入背景, |
|
|
|
* 默认提供一个简单的灰化处理 |
|
|
|
* 暂时用作灰度,后面再处理 |
|
|
|
|
|
|
|
*/ |
|
|
|
*/ |
|
|
|
private BufferedImage toGrayImage(Dimension size, Color background) { |
|
|
|
@Override |
|
|
|
SvgTranscoder transcoder = new SvgTranscoder(size, background, true); |
|
|
|
public @NotNull SvgIcon white() { |
|
|
|
TranscoderInput transcoderInput = new TranscoderInput(resource.getInputStream()); |
|
|
|
return new SvgIcon(resource, size, Type.white); |
|
|
|
try { |
|
|
|
|
|
|
|
transcoder.transcode(transcoderInput, null); |
|
|
|
|
|
|
|
return transcoder.getImage(); |
|
|
|
|
|
|
|
} catch (TranscoderException e) { |
|
|
|
|
|
|
|
throw new RuntimeException(e); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* 默认提供一个简单的灰化处理 |
|
|
|
* 默认提供一个简单的灰化处理 |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
@Override |
|
|
|
@Override |
|
|
|
public @NotNull SvgIcon disabled() { |
|
|
|
public @NotNull SvgIcon disabled() { |
|
|
|
return new SvgIcon(resource, size, true); |
|
|
|
return new SvgIcon(resource, size, Type.disable); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|