SvgIcon.java

  1. package swingtree.style;

  2. import com.github.weisj.jsvg.SVGDocument;
  3. import com.github.weisj.jsvg.parser.LoaderContext;
  4. import com.github.weisj.jsvg.parser.SVGLoader;
  5. import com.github.weisj.jsvg.view.FloatSize;
  6. import com.github.weisj.jsvg.view.ViewBox;
  7. import org.jspecify.annotations.Nullable;
  8. import org.slf4j.Logger;
  9. import swingtree.UI;
  10. import swingtree.layout.Size;

  11. import javax.swing.ImageIcon;
  12. import javax.swing.JComponent;
  13. import javax.swing.border.Border;
  14. import java.awt.*;
  15. import java.awt.geom.AffineTransform;
  16. import java.awt.image.BufferedImage;
  17. import java.io.InputStream;
  18. import java.net.URL;
  19. import java.util.Objects;
  20. import java.util.Optional;

  21. /**
  22.  *   A specialized {@link ImageIcon} subclass that allows you to use SVG based icon images in your GUI.
  23.  *   This in essence just a wrapper around the <a href="https://github.com/weisJ/jsvg">JSVG library</a>,
  24.  *   which renders SVG images using the Java2D graphics API.
  25.  *   <p>
  26.  *   You may use this like a regular {@link ImageIcon}, but have to keep in mind that SVG documents
  27.  *   do not really have a fixed size, meaning that the {@link #getIconWidth()} and {@link #getIconHeight()}
  28.  *   on a freshly loaded {@link SvgIcon} will return -1 which causes the icon to be rendered according to the
  29.  *   width and height of the component it is rendered into (see {@link #paintIcon(Component, java.awt.Graphics, int, int)}).
  30.  *   <p>
  31.  *   If you want to render the icon with a fixed size, you can use the {@link #withIconWidth(int)} and
  32.  *   {@link #withIconHeight(int)} or {@link #withIconSize(int, int)} methods to create a new {@link SvgIcon}
  33.  *   with the given width and height.
  34.  *   <p>
  35.  *   An {@link SvgIcon} with an undefined width or height will also be using the {@link UI.FitComponent}
  36.  *   and {@link UI.Placement} policies to determine how the icon should be placed and sized within a component.
  37.  *   Use the {@link #withFitComponent(UI.FitComponent)} and {@link #withPreferredPlacement(UI.Placement)}
  38.  *   methods to create a new {@link SvgIcon} with the given policies and use the {@link #getFitComponent()}
  39.  *   and {@link #getPreferredPlacement()} methods to retrieve the current policies
  40.  *   (Not that these will not have any effect if the width and height are both defined).
  41.  *   <p>
  42.  *   <b>Also note that the direct use of this class and it's API is discouraged in favour of simply
  43.  *   calling the {@link UI#findIcon(String)} or {@link UI#findSvgIcon(String)} methods, which
  44.  *   will automatically load and cache all of the icons for you.</b>
  45.  */
  46. public final class SvgIcon extends ImageIcon
  47. {
  48.     private static final Logger log = org.slf4j.LoggerFactory.getLogger(SvgIcon.class);
  49.     private static final UI.FitComponent DEFAULT_FIT_COMPONENT = UI.FitComponent.MIN_DIM;
  50.     private static final UI.Placement    DEFAULT_PLACEMENT     = UI.Placement.UNDEFINED;
  51.     private static final int             NO_SIZE               = -1;
  52.     private static final Insets          ZERO_INSETS           = new Insets(0,0,0,0);


  53.     private final @Nullable SVGDocument _svgDocument;
  54.     private final Size                  _size;
  55.     private final UI.FitComponent       _fitComponent;
  56.     private final UI.Placement          _preferredPlacement;

  57.     private @Nullable BufferedImage _cache = null;


  58.     private SvgIcon(
  59.         @Nullable SVGDocument svgDocument, // nullable
  60.         Size                  size,
  61.         UI.FitComponent       fitComponent,
  62.         UI.Placement          preferredPlacement
  63.     ) {
  64.         super();
  65.         _svgDocument        = svgDocument;
  66.         _size               = Objects.requireNonNull(size);
  67.         _fitComponent       = Objects.requireNonNull(fitComponent);
  68.         _preferredPlacement = Objects.requireNonNull(preferredPlacement);
  69.     }

  70.     /**
  71.      * @param path The path to the SVG document.
  72.      */
  73.     public SvgIcon( String path ) {
  74.         this(_loadSvgDocument(SvgIcon.class.getResource(path)), Size.unknown(), DEFAULT_FIT_COMPONENT, DEFAULT_PLACEMENT);
  75.     }

  76.     /**
  77.      * @param svgUrl The URL to the SVG document.
  78.      */
  79.     public SvgIcon( URL svgUrl ) {
  80.         this(_loadSvgDocument(svgUrl), Size.unknown(), DEFAULT_FIT_COMPONENT, DEFAULT_PLACEMENT);
  81.     }

  82.     /**
  83.      * @param stream The input stream supplying the text data of the SVG document.
  84.      */
  85.     public SvgIcon( InputStream stream ) {
  86.         this(_loadSvgDocument(stream), Size.unknown(), DEFAULT_FIT_COMPONENT, DEFAULT_PLACEMENT);
  87.     }

  88.     /**
  89.      * @param svgDocument The already loaded SVG document, which will be used to render the icon.
  90.      */
  91.     public SvgIcon( SVGDocument svgDocument ) {
  92.         this(svgDocument, Size.unknown(), DEFAULT_FIT_COMPONENT, DEFAULT_PLACEMENT);
  93.     }

  94.     /**
  95.      * @param path The path to the SVG document.
  96.      * @param size The size of the icon in the form of a {@link Dimension}.
  97.      */
  98.     public SvgIcon( String path, Dimension size ) {
  99.         this(_loadSvgDocument(SvgIcon.class.getResource(path)), Size.of(size), DEFAULT_FIT_COMPONENT, DEFAULT_PLACEMENT);
  100.     }

  101.     /**
  102.      * @param svgUrl The URL to the SVG document.
  103.      * @param size The size of the icon in the form of a {@link Dimension}.
  104.      */
  105.     public SvgIcon( URL svgUrl, Dimension size ) {
  106.         this(_loadSvgDocument(svgUrl), Size.of(size), DEFAULT_FIT_COMPONENT, DEFAULT_PLACEMENT);
  107.     }

  108.     /**
  109.      * @param stream The input stream supplying the text data of the SVG document.
  110.      * @param size The size of the icon in the form of a {@link Dimension}.
  111.      */
  112.     public SvgIcon( InputStream stream, Dimension size ) {
  113.         this(_loadSvgDocument(stream), Size.of(size), DEFAULT_FIT_COMPONENT, DEFAULT_PLACEMENT);
  114.     }

  115.     /**
  116.      * @param svgDocument The already loaded SVG document, which will be used to render the icon.
  117.      * @param size The size of the icon in the form of a {@link Dimension}.
  118.      */
  119.     public SvgIcon( SVGDocument svgDocument, Dimension size ) {
  120.         this(svgDocument, Size.of(size), DEFAULT_FIT_COMPONENT, DEFAULT_PLACEMENT);
  121.     }


  122.     private static @Nullable SVGDocument _loadSvgDocument( URL svgUrl ) {
  123.         SVGDocument tempSVGDocument = null;
  124.         try {
  125.             SVGLoader loader = new SVGLoader();
  126.             tempSVGDocument = loader.load(svgUrl);
  127.         } catch (Exception e) {
  128.             log.error("Failed to load SVG document from URL: " + svgUrl, e);
  129.         }
  130.         return tempSVGDocument;
  131.     }

  132.     private static @Nullable SVGDocument _loadSvgDocument( InputStream stream ) {
  133.         SVGDocument tempSVGDocument = null;
  134.         try {
  135.             SVGLoader loader = new SVGLoader();
  136.             tempSVGDocument = loader.load(stream, null, LoaderContext.createDefault());
  137.         } catch (Exception e) {
  138.             log.error("Failed to load SVG document from stream: " + stream, e);
  139.         }
  140.         return tempSVGDocument;
  141.     }

  142.     /**
  143.      *  Exposes the width of the icon, or -1 if the icon should be rendered according
  144.      *  to the width of a given component or the width of the SVG document itself.
  145.      *  (...or other policies such as {@link swingtree.UI.FitComponent} and {@link swingtree.UI.Placement}).<br>
  146.      *  <b>
  147.      *      Note that the returned width is dynamically scaled according to
  148.      *      the current {@link swingtree.UI#scale()} value.
  149.      *      This is to ensure that the icon is rendered at the correct size
  150.      *      according to the current DPI settings.
  151.      *      If you want the unscaled width, use {@link #getBaseWidth()}.
  152.      *  </b>
  153.      * @return The width of the icon, or -1 if the icon should be rendered according
  154.      *         to the width of a given component or the width of the SVG document itself.
  155.      */
  156.     @Override
  157.     public int getIconWidth() {
  158.         Size adjustedSize = _sizeWithAspectRatioCorrection(_size);
  159.         return adjustedSize.width().map(UI::scale).map(Math::round).orElse(NO_SIZE);
  160.     }

  161.     /**
  162.      *  Creates an updated {@link SvgIcon} with the given width returned by {@link #getIconWidth()}.
  163.      *
  164.      * @param newWidth The width of the icon, or -1 if the icon should be rendered according
  165.      *              to the width of a given component or the width of the SVG document itself.
  166.      * @return A new {@link SvgIcon} with the given width.
  167.      *        If the width is -1, the icon will be rendered according to the width of a given component
  168.      *        or the width of the SVG document itself.
  169.      */
  170.     public SvgIcon withIconWidth( int newWidth ) {
  171.         int width = _size.width().map(Math::round).orElse(NO_SIZE);
  172.         if ( newWidth == width )
  173.             return this;
  174.         return new SvgIcon(_svgDocument, _size.withWidth(newWidth), _fitComponent, _preferredPlacement);
  175.     }

  176.     /**
  177.      *  Exposes the height of the icon, or -1 if the icon should be rendered according
  178.      *  to the height of a given component or the height of the SVG document itself.
  179.      *  (...or other policies such as {@link swingtree.UI.FitComponent} and {@link swingtree.UI.Placement}).<br>
  180.      *  <b>
  181.      *      Note that the returned height is dynamically scaled according to
  182.      *      the current {@link swingtree.UI#scale()} value.
  183.      *      This is to ensure that the icon is rendered at the correct size
  184.      *      according to the current DPI settings.
  185.      *      If you want the unscaled height, use {@link #getBaseHeight()}.
  186.      *  </b>
  187.      *
  188.      * @return A new {@link SvgIcon} with the given width and height.
  189.      *        If the width or height is -1, the icon will be rendered according to the width or height of a given component
  190.      *        or the width or height of the SVG document itself.
  191.      */
  192.     @Override
  193.     public int getIconHeight() {
  194.         Size adjustedSize = _sizeWithAspectRatioCorrection(_size);
  195.         return adjustedSize.height().map(UI::scale).map(Math::round).orElse(NO_SIZE);
  196.     }

  197.     /**
  198.      *  Exposes the fixed width defined for the icon, which is the width
  199.      *  that was set when the icon was created or updated using the
  200.      *  {@link #withIconWidth(int)} method.<br>
  201.      *  <b>
  202.      *      Note that this width is not scaled according to the current {@link swingtree.UI#scale()} value.
  203.      *      If you want a scaled width, that is more suitable for rendering the icon,
  204.      *      use the {@link #getIconWidth()} method.
  205.      *  </b>
  206.      *
  207.      * @return The width of the icon without scaling.
  208.      */
  209.     public int getBaseWidth() {
  210.         return _size.width().map(Math::round).orElse(NO_SIZE);
  211.     }

  212.     /**
  213.      *  Exposes the fixed height defined for the icon, which is the height
  214.      *  that was set when the icon was created or updated using the
  215.      *  {@link #withIconHeight(int)} method.<br>
  216.      *  <b>
  217.      *      Note that this height is not scaled according to the current {@link swingtree.UI#scale()} value.
  218.      *      If you want a scaled height, that is more suitable for rendering the icon,
  219.      *      use the {@link #getIconHeight()} method.
  220.      *  </b>
  221.      *
  222.      * @return The height of the icon without scaling.
  223.      */
  224.     public int getBaseHeight() {
  225.         return _size.height().map(Math::round).orElse(NO_SIZE);
  226.     }

  227.     /**
  228.      *  Creates an updated {@link SvgIcon} with the supplied integer used
  229.      *  as the icon height, which you can retrieve using {@link #getIconHeight()}.
  230.      *  If the height is -1, the icon will be rendered according to the height of a given component
  231.      *  or the height of the SVG document itself.
  232.      *  (...or other policies such as {@link swingtree.UI.FitComponent} and {@link swingtree.UI.Placement}).
  233.      *
  234.      * @param height The height of the icon, or -1 if the icon should be rendered according
  235.      *               to the height of a given component or the height of the SVG document itself.
  236.      * @return A new {@link SvgIcon} with the given height.
  237.      *        If the height is -1, the icon will be rendered according to the height of a given component
  238.      *        or the height of the SVG document itself.
  239.      */
  240.     public SvgIcon withIconHeight( int height ) {
  241.         int currentHeight = _size.height().map(Math::round).orElse(NO_SIZE);
  242.         if ( height == currentHeight )
  243.             return this;
  244.         return new SvgIcon(_svgDocument, _size.withHeight(height), _fitComponent, _preferredPlacement);
  245.     }

  246.     /**
  247.      *  Creates an updated {@link SvgIcon} with the given width and height.
  248.      *  Dimensions smaller than 0 are considered "undefined".
  249.      *  When the icon is being rendered then these will be determined according to the
  250.      *  aspect ratio of the SVG document, the {@link swingtree.UI.FitComponent} / {@link swingtree.UI.Placement}
  251.      *  policies or the size of the component the SVG is rendered into.
  252.      *
  253.      * @param newWidth The width of the icon, or -1 if the icon should be rendered according
  254.      *              to the width of a given component or the width of the SVG document itself.
  255.      * @param newHeight The height of the icon, or -1 if the icon should be rendered according
  256.      *               to the height of a given component or the height of the SVG document itself.
  257.      * @return A new {@link SvgIcon} with the given width and height.
  258.      *        If the width or height is -1, the icon will be rendered according to the width or height of a given component
  259.      *        or the width or height of the SVG document itself.
  260.      */
  261.     public SvgIcon withIconSize( int newWidth, int newHeight ) {
  262.         newWidth  = newWidth  < 0 ? NO_SIZE : newWidth;
  263.         newHeight = newHeight < 0 ? NO_SIZE : newHeight;
  264.         int width  = _size.width().map(Math::round).orElse(NO_SIZE);
  265.         int height = _size.height().map(Math::round).orElse(NO_SIZE);
  266.         if ( newWidth == width && newHeight == height )
  267.             return this;
  268.         return new SvgIcon(_svgDocument, Size.of(newWidth, newHeight), _fitComponent, _preferredPlacement);
  269.     }

  270.     /**
  271.      *  Allows you to create an updated {@link SvgIcon} with the given size
  272.      *  in the form of a {@link Size} object containing the width and height.
  273.      *  If the width or height is -1, the icon will be rendered according to the
  274.      *  width or height of a given component, the width or height of the SVG document
  275.      *  and the {@link swingtree.UI.FitComponent} / {@link swingtree.UI.Placement} policies.
  276.      *
  277.      * @param size The size of the icon in the form of a {@link Size}.
  278.      * @return A new {@link SvgIcon} with the given width and height.
  279.      */
  280.     public SvgIcon withIconSize( Size size ) {
  281.         return withIconSize(
  282.                     size.width().map(Math::round).orElse(NO_SIZE),
  283.                     size.height().map(Math::round).orElse(NO_SIZE)
  284.                 );
  285.     }

  286.     /**
  287.      *  Determines the size of the icon (both width and height) using the provided width
  288.      *  and the aspect ratio of the SVG document.
  289.      *  If the width is -1, the icon will lose its fixed width and will
  290.      *  be rendered according to the width of a given component.
  291.      *  <p>
  292.      *  For example, if the SVG document has an aspect ratio of 2:1, and the width is 200,
  293.      *  then the height will be 100.
  294.      *  <p>
  295.      *  Also see {@link #withIconSizeFromHeight(int)}.
  296.      *
  297.      * @param newWidth The width of the icon, or -1 if the icon should be rendered according
  298.      *              to the width of a given component or the width of the SVG document itself.
  299.      * @return A new {@link SvgIcon} with the given width and a logical height that is
  300.      *         determined by the aspect ratio of the SVG document.
  301.      */
  302.     public SvgIcon withIconSizeFromWidth( int newWidth ) {
  303.         if ( newWidth < 0 )
  304.             return this.withIconSize(NO_SIZE, NO_SIZE);

  305.         Size adjustedSize = _sizeWithAspectRatioCorrection(Size.unknown().withWidth(newWidth));
  306.         return new SvgIcon(_svgDocument, adjustedSize, _fitComponent, _preferredPlacement);
  307.     }

  308.     /**
  309.      *  Determines the size of the icon (both width and height) using the provided height
  310.      *  and the aspect ratio of the SVG document.
  311.      *  If the height is -1, the icon will lose its fixed height and will
  312.      *  be rendered according to the height of a given component.
  313.      *  <p>
  314.      *  For example, if the SVG document has an aspect ratio of 2:1, and the height is 100,
  315.      *  then the width will be 200.
  316.      *  <p>
  317.      *  Also see {@link #withIconSizeFromWidth(int)}.
  318.      *
  319.      * @param newHeight The height of the icon, or -1 if the icon should be rendered according
  320.      *               to the height of a given component or the height of the SVG document itself.
  321.      * @return A new {@link SvgIcon} with the given height and a logical width that is
  322.      *         determined by the aspect ratio of the SVG document.
  323.      */
  324.     public SvgIcon withIconSizeFromHeight( int newHeight ) {
  325.         if ( newHeight < 0 )
  326.             return this.withIconSize(NO_SIZE, NO_SIZE);

  327.         Size adjustedSize = _sizeWithAspectRatioCorrection(Size.unknown().withHeight(newHeight));
  328.         return new SvgIcon(_svgDocument, adjustedSize, _fitComponent, _preferredPlacement);
  329.     }

  330.     /**
  331.      *  The underlying SVG document contains a size object, which
  332.      *  is the width and height of the root SVG element inside the document.
  333.      *
  334.      * @return The size of the SVG document in the form of a {@link Size},
  335.      *         a subclass of {@link java.awt.geom.Dimension2D}.
  336.      */
  337.     public Size getSvgSize() {
  338.         if ( _svgDocument == null )
  339.             return Size.unknown();
  340.         FloatSize svgSize = _svgDocument.size();
  341.         return Size.of(svgSize.width, svgSize.height);
  342.     }

  343.     /**
  344.      *  Allows you to access the underlying {@link SVGDocument} that is used to render the icon.
  345.      *
  346.      * @return The underlying {@link SVGDocument} that is used to render the icon.
  347.      */
  348.     public @Nullable SVGDocument getSvgDocument() {
  349.         return _svgDocument;
  350.     }

  351.     /**
  352.      *  Allows you to access the {@link UI.FitComponent} policy, which
  353.      *  determines if and how the icon should be fitted
  354.      *  onto a component when rendered through the
  355.      *  {@link #paintIcon(Component, java.awt.Graphics, int, int, int, int)} method.<br>
  356.      *  The following fit modes are available:
  357.      *  <ul>
  358.      *      <li>{@link UI.FitComponent#NO} -
  359.      *      The image will not be scaled to fit the inner component area.
  360.      *      </li>
  361.      *      <li>{@link UI.FitComponent#WIDTH} -
  362.      *      The image will be scaled to fit the inner component width.
  363.      *      </li>
  364.      *      <li>{@link UI.FitComponent#HEIGHT} -
  365.      *      The image will be scaled to fit the inner component height.
  366.      *      </li>
  367.      *      <li>{@link UI.FitComponent#WIDTH_AND_HEIGHT} -
  368.      *      The image will be scaled to fit both the component width and height.
  369.      *      </li>
  370.      *      <li>{@link UI.FitComponent#MAX_DIM} -
  371.      *      The image will be scaled to fit the larger of the two dimensions of the inner component area.
  372.      *      </li>
  373.      *      <li>{@link UI.FitComponent#MIN_DIM} -
  374.      *      The image will be scaled to fit the smaller of the two dimensions of the inner component area.
  375.      *      </li>
  376.      *  </ul>
  377.      *  See {@link #withFitComponent(UI.FitComponent)} if you want to create a new {@link SvgIcon}
  378.      *  with an updated fit policy.
  379.      *
  380.      * @return The {@link UI.FitComponent} that determines if and how the icon should be fitted into a
  381.      *         any given component (see {@link #paintIcon(Component, java.awt.Graphics, int, int, int, int)}).
  382.      */
  383.     public UI.FitComponent getFitComponent() { return _fitComponent; }

  384.     /**
  385.      *  There are different kinds of strategies to fit an SVG icon onto the component.
  386.      *  These strategies are identified using the {@link UI.FitComponent} enum
  387.      *  which defines the following fit modes:
  388.      *  <ul>
  389.      *      <li>{@link UI.FitComponent#NO} -
  390.      *          The image will not be scaled to fit the inner component area.
  391.      *      </li>
  392.      *      <li>{@link UI.FitComponent#WIDTH} -
  393.      *          The image will be scaled to fit the inner component width.
  394.      *      </li>
  395.      *      <li>{@link UI.FitComponent#HEIGHT} -
  396.      *          The image will be scaled to fit the inner component height.
  397.      *      </li>
  398.      *      <li>{@link UI.FitComponent#WIDTH_AND_HEIGHT} -
  399.      *          The image will be scaled to fit both the component width and height.
  400.      *      </li>
  401.      *      <li>{@link UI.FitComponent#MAX_DIM} -
  402.      *          The image will be scaled to fit the larger
  403.      *          of the two dimension of the inner component area.
  404.      *      </li>
  405.      *      <li>{@link UI.FitComponent#MIN_DIM} -
  406.      *          The image will be scaled to fit the smaller
  407.      *          of the two dimension of the inner component area.
  408.      *      </li>
  409.      *  </ul>
  410.      * @param fit The {@link UI.FitComponent} that determines if and how the icon should be fitted into a
  411.      *            any given component (see {@link #paintIcon(Component, java.awt.Graphics, int, int, int, int)}).
  412.      * @return A new {@link SvgIcon} with the given {@link UI.FitComponent} policy.
  413.      */
  414.     public SvgIcon withFitComponent( UI.FitComponent fit ) {
  415.         Objects.requireNonNull(fit);
  416.         if ( fit == _fitComponent )
  417.             return this;
  418.         return new SvgIcon(_svgDocument, _size, fit, _preferredPlacement);
  419.     }

  420.     /**
  421.      *  The preferred placement policy determines where the icon
  422.      *  should be placed within a component when rendered through the
  423.      *  {@link #paintIcon(Component, java.awt.Graphics, int, int, int, int)} method.
  424.      *
  425.      * @return The {@link UI.Placement} that determines where the icon should be placed within a component
  426.      *         (see {@link #paintIcon(Component, java.awt.Graphics, int, int, int, int)}).
  427.      */
  428.     public UI.Placement getPreferredPlacement() { return _preferredPlacement; }

  429.     /**
  430.      *  Allows you to get an updated {@link SvgIcon} with the given {@link UI.Placement} policy
  431.      *  which determines where the icon should be placed within a component
  432.      *  when rendered through the {@link #paintIcon(Component, java.awt.Graphics, int, int, int, int)} method.
  433.      *
  434.      * @param placement The {@link UI.Placement} that determines where the icon should be placed within a component
  435.      *                  (see {@link #paintIcon(Component, java.awt.Graphics, int, int, int, int)}).
  436.      * @return A new {@link SvgIcon} with the given {@link UI.Placement} policy.
  437.      */
  438.     public SvgIcon withPreferredPlacement( UI.Placement placement ) {
  439.         Objects.requireNonNull(placement);
  440.         if ( placement == _preferredPlacement )
  441.             return this;
  442.         return new SvgIcon(_svgDocument, _size, _fitComponent, placement);
  443.     }

  444.     /**
  445.      *  Creates a new {@link Image} from the SVG document.
  446.      * @return A new {@link Image} where the SVG document has been rendered into.
  447.      */
  448.     @Override
  449.     public Image getImage() {

  450.         if ( _cache != null )
  451.             return _cache;

  452.         int width  = getIconWidth();
  453.         int height = getIconHeight();

  454.         if ( _svgDocument != null ) {
  455.             if (width < 0)
  456.                 width = (int) UI.scale(_svgDocument.size().width);
  457.             if (height < 0)
  458.                 height = (int) UI.scale(_svgDocument.size().height);
  459.         }

  460.         // We create a new buffered image, render into it, and then return it.
  461.         BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
  462.         if ( _svgDocument != null )
  463.             _svgDocument.render(
  464.                     null,
  465.                     image.createGraphics(),
  466.                     new ViewBox(0, 0, width, height)
  467.                 );

  468.         return image;
  469.     }

  470.     /**
  471.      *  We don't support this. An SVG document is not really an image, it's a vector graphic.
  472.      *  Extending {@link ImageIcon} is just for compatibility reasons...
  473.      */
  474.     @Override
  475.     public void setImage( Image image ) {
  476.         // We don't support this.
  477.     }

  478.     /**
  479.      * @param c The component to render the icon into.
  480.      * @param g the graphics context
  481.      * @param x the X coordinate of the icon's top-left corner
  482.      * @param y the Y coordinate of the icon's top-left corner
  483.      */
  484.     @Override
  485.     public synchronized void paintIcon( java.awt.Component c, java.awt.Graphics g, int x, int y )
  486.     {
  487.         if ( _svgDocument == null )
  488.             return;

  489.         int scaledWidth  = getIconWidth();
  490.         int scaledHeight = getIconHeight();

  491.         UI.Placement preferredPlacement = _preferredPlacement;

  492.         if ( preferredPlacement == UI.Placement.UNDEFINED && c instanceof JComponent )
  493.             preferredPlacement = ComponentExtension.from((JComponent) c).preferredIconPlacement();

  494.         Insets insets = ZERO_INSETS;

  495.         if ( c != null ) {
  496.             /*
  497.                 If the component exists we want to account for its (border) insets.
  498.                 This is to avoid the icon colliding with the component border
  499.                 and also to make sure the icon is centered properly.
  500.             */
  501.             insets = Optional.ofNullable( c instanceof JComponent ? ((JComponent)c).getBorder() : null )
  502.                                 .map( b -> {
  503.                                     try {
  504.                                         return _determineInsetsForBorder(b, c);
  505.                                     } catch (Exception e) {
  506.                                         return ZERO_INSETS;
  507.                                     }
  508.                                 })
  509.                                 .orElse(ZERO_INSETS);

  510.             if ( scaledWidth < 0 )
  511.                 x = insets.left;

  512.             if ( scaledHeight < 0 )
  513.                 y = insets.top;
  514.         }

  515.         int width  = Math.max( scaledWidth,  c == null ? NO_SIZE : c.getWidth()  );
  516.         int height = Math.max( scaledHeight, c == null ? NO_SIZE : c.getHeight() );

  517.         width  = scaledWidth  >= 0 ? scaledWidth  : width  - insets.right  - insets.left;
  518.         height = scaledHeight >= 0 ? scaledHeight : height - insets.bottom - insets.top ;

  519.         if ( width  <= 0 ) {
  520.             int smaller = (int) Math.floor( width / 2.0 );
  521.             int larger  = (int) Math.ceil(  width / 2.0 );
  522.             x += smaller;
  523.             width = ( larger - smaller );
  524.         }
  525.         if ( height <= 0 ) {
  526.             int smaller = (int) Math.floor( height / 2.0 );
  527.             int larger  = (int) Math.ceil(  height / 2.0 );
  528.             y += smaller;
  529.             height = ( larger - smaller );
  530.         }

  531.         if ( scaledWidth > 0 && scaledHeight > 0 ) {
  532.             if ( _cache != null && _cache.getWidth() == width && _cache.getHeight() == height )
  533.                 g.drawImage(_cache, x, y, width, height, null);
  534.             else {
  535.                 _cache = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
  536.                 paintIcon(c, _cache.getGraphics(), 0, 0, width, height);
  537.                 g.drawImage(_cache, x, y, width, height, null);
  538.             }
  539.         }
  540.         else
  541.             _paintIcon( c, g, x, y, width, height, preferredPlacement );
  542.     }

  543.     private Insets _determineInsetsForBorder( Border b, Component c )
  544.     {
  545.         if ( b == null )
  546.             return ZERO_INSETS;

  547.         if ( b instanceof StyleAndAnimationBorder )
  548.             return ((StyleAndAnimationBorder<?>)b).getFullPaddingInsets();

  549.         // Compound border
  550.         if ( b instanceof javax.swing.border.CompoundBorder ) {
  551.             javax.swing.border.CompoundBorder cb = (javax.swing.border.CompoundBorder) b;
  552.             return cb.getOutsideBorder().getBorderInsets(c);
  553.         }

  554.         try {
  555.             return b.getBorderInsets(c);
  556.         } catch (Exception e) {
  557.             // Ignore
  558.         }
  559.         return ZERO_INSETS;
  560.     }

  561.     void paintIcon(
  562.             final @Nullable Component c,
  563.             final java.awt.Graphics g,
  564.             int x,
  565.             int y,
  566.             int width,
  567.             int height
  568.     ) {
  569.         _paintIcon( c, g, x, y, width, height, _preferredPlacement );
  570.     }

  571.     private void _paintIcon(
  572.         final @Nullable Component c,
  573.         final java.awt.Graphics g,
  574.         int x,
  575.         int y,
  576.         int width,
  577.         int height,
  578.         UI.Placement preferredPlacement
  579.     ) {
  580.         if ( _svgDocument == null )
  581.             return;

  582.         int scaledWidth  = getIconWidth();
  583.         int scaledHeight = getIconHeight();

  584.         width  = ( width  < 0 ? scaledWidth  : width  );
  585.         height = ( height < 0 ? scaledHeight : height );

  586.         Graphics2D g2d = (Graphics2D) g.create();

  587.         FloatSize svgSize = _svgDocument.size();
  588.         float svgRefWidth  = ( svgSize.width  > svgSize.height ? 1f : svgSize.width  / svgSize.height );
  589.         float svgRefHeight = ( svgSize.height > svgSize.width  ? 1f : svgSize.height / svgSize.width  );
  590.         float imgRefWidth  = (     width      >=   height      ? 1f : (float) width  /   height       );
  591.         float imgRefHeight = (     height     >=   width       ? 1f : (float) height /   width        );

  592.         float scaleX = imgRefWidth  / svgRefWidth;
  593.         float scaleY = imgRefHeight / svgRefHeight;

  594.         if ( _fitComponent == UI.FitComponent.MIN_DIM || _fitComponent == UI.FitComponent.MAX_DIM ) {
  595.             if ( width < height )
  596.                 scaleX = 1f;
  597.             if ( height < width )
  598.                 scaleY = 1f;
  599.         }

  600.         if ( _fitComponent == UI.FitComponent.WIDTH )
  601.             scaleX = 1f;

  602.         if ( _fitComponent == UI.FitComponent.HEIGHT )
  603.             scaleY = 1f;

  604.         ViewBox viewBox = new ViewBox(x, y, width, height);
  605.         float boxX      = viewBox.x  / scaleX;
  606.         float boxY      = viewBox.y  / scaleY;
  607.         float boxWidth  = viewBox.width  / scaleX;
  608.         float boxHeight = viewBox.height / scaleY;
  609.         if ( _fitComponent == UI.FitComponent.MAX_DIM ) {
  610.             // We now want to make sure that the
  611.             if ( boxWidth < boxHeight ) {
  612.                 // We find the scale factor of the heights between the two rectangles:
  613.                 float scaleHeight = ( viewBox.height / svgSize.height );
  614.                 // We now want to scale the view box so that both have the same heights:
  615.                 float newWidth  = svgSize.width  * scaleHeight;
  616.                 float newHeight = svgSize.height * scaleHeight;
  617.                 float newX = viewBox.x + (viewBox.width - newWidth) / 2f;
  618.                 float newY = viewBox.y;
  619.                 viewBox = new ViewBox(newX, newY, newWidth, newHeight);
  620.             } else {
  621.                 // We find the scale factor of the widths between the two rectangles:
  622.                 float scaleWidth = ( viewBox.width / svgSize.width );
  623.                 // We now want to scale the view box so that both have the same widths:
  624.                 float newWidth  = svgSize.width  * scaleWidth;
  625.                 float newHeight = svgSize.height * scaleWidth;
  626.                 float newX = viewBox.x;
  627.                 float newY = viewBox.y + (viewBox.height - newHeight) / 2f;
  628.                 viewBox = new ViewBox(newX, newY, newWidth, newHeight);
  629.             }
  630.         }
  631.         else
  632.             viewBox = new ViewBox(boxX, boxY, boxWidth, boxHeight);

  633.         if ( _fitComponent == UI.FitComponent.NO ) {
  634.             width   = scaledWidth  >= 0 ? scaledWidth  : (int) svgSize.width;
  635.             height  = scaledHeight >= 0 ? scaledHeight : (int) svgSize.height;
  636.             viewBox = new ViewBox( x, y, width, height );
  637.         }

  638.         // Let's check if the view box exists:
  639.         if ( viewBox.width <= 0 || viewBox.height <= 0 )
  640.             return;

  641.         // Also let's check if the view box has valid values:
  642.         if ( Float.isNaN(viewBox.x) || Float.isNaN(viewBox.y) || Float.isNaN(viewBox.width) || Float.isNaN(viewBox.height) )
  643.             return;

  644.         // Let's make sure the view box has the correct dimension ratio:
  645.         float viewBoxRatio = _svgDocument.size().width / _svgDocument.size().height;
  646.         float boxRatio     =      viewBox.width        /      viewBox.height;
  647.         if ( boxRatio > viewBoxRatio ) {
  648.             // The view box is too wide, we need to make it narrower:
  649.             float newWidth = viewBox.height * viewBoxRatio;
  650.             viewBox = new ViewBox( viewBox.x + (viewBox.width - newWidth) / 2f, viewBox.y, newWidth, viewBox.height );
  651.         }
  652.         if ( boxRatio < viewBoxRatio ) {
  653.             // The view box is too tall, we need to make it shorter:
  654.             float newHeight = viewBox.width / viewBoxRatio;
  655.             viewBox = new ViewBox( viewBox.x, viewBox.y + (viewBox.height - newHeight) / 2f, viewBox.width, newHeight );
  656.         }

  657.         /*
  658.             Before we do the actual rendering we first check if there
  659.             is a preferred placement that is not the center.
  660.             If that is the case we move the view box accordingly.
  661.          */
  662.         if ( preferredPlacement != UI.Placement.UNDEFINED && preferredPlacement != UI.Placement.CENTER ) {
  663.             // First we correct if the component area is smaller than the view box:
  664.             width += (int) Math.max(0, ( viewBox.x + viewBox.width ) - ( x + width ) );
  665.             width += (int) Math.max(0, x - viewBox.x );
  666.             x = (int) Math.min(x, viewBox.x);
  667.             height += (int) Math.max(0, ( viewBox.y + viewBox.height ) - ( y + height ) );
  668.             height += (int) Math.max(0, y - viewBox.y );
  669.             y = (int) Math.min(y, viewBox.y);

  670.             switch ( preferredPlacement ) {
  671.                 case TOP_LEFT:
  672.                     viewBox = new ViewBox( x, y, viewBox.width, viewBox.height );
  673.                     break;
  674.                 case TOP_RIGHT:
  675.                     viewBox = new ViewBox( x + width - viewBox.width, y, viewBox.width, viewBox.height );
  676.                     break;
  677.                 case BOTTOM_LEFT:
  678.                     viewBox = new ViewBox( x, y + height - viewBox.height, viewBox.width, viewBox.height );
  679.                     break;
  680.                 case BOTTOM_RIGHT:
  681.                     viewBox = new ViewBox( x + width - viewBox.width, y + height - viewBox.height, viewBox.width, viewBox.height );
  682.                     break;
  683.                 case TOP:
  684.                     viewBox = new ViewBox( x + (width - viewBox.width) / 2f, y, viewBox.width, viewBox.height );
  685.                     break;
  686.                 case BOTTOM:
  687.                     viewBox = new ViewBox( x + (width - viewBox.width) / 2f, y + height - viewBox.height, viewBox.width, viewBox.height );
  688.                     break;
  689.                 case LEFT:
  690.                     viewBox = new ViewBox( x, y + (height - viewBox.height) / 2f, viewBox.width, viewBox.height );
  691.                     break;
  692.                 case RIGHT:
  693.                     viewBox = new ViewBox( x + width - viewBox.width, y + (height - viewBox.height) / 2f, viewBox.width, viewBox.height );
  694.                     break;
  695.                 default:
  696.                     log.warn("Unknown preferred placement: " + preferredPlacement);
  697.             }
  698.         }

  699.         // Now onto the actual rendering:

  700.         boolean doAntiAliasing  = StyleEngine.IS_ANTIALIASING_ENABLED();
  701.         boolean wasAntiAliasing = g2d.getRenderingHint( java.awt.RenderingHints.KEY_ANTIALIASING ) == java.awt.RenderingHints.VALUE_ANTIALIAS_ON;
  702.         if ( doAntiAliasing && !wasAntiAliasing )
  703.             g2d.setRenderingHint( java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON );

  704.         boolean needsScaling = ( scaleX != 1 || scaleY != 1 );
  705.         AffineTransform oldTransform = g2d.getTransform();

  706.         if ( needsScaling ) {
  707.             AffineTransform newTransform = new AffineTransform(oldTransform);
  708.             newTransform.scale(scaleX, scaleY);
  709.             g2d.setTransform(newTransform);
  710.         }

  711.         try {
  712.             // We also have to scale x and y, this is because the SVGDocument does not
  713.             // account for the scale of the transform with respect to the view box!
  714.             _svgDocument.render((JComponent) c, g2d, viewBox);
  715.         } catch (Exception e) {
  716.             log.warn("Failed to render SVG document.", e);
  717.         }

  718.         if ( needsScaling )
  719.             g2d.setTransform(oldTransform); // back to the previous scaling!

  720.         if ( doAntiAliasing && !wasAntiAliasing )
  721.             g2d.setRenderingHint( java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_OFF );
  722.     }

  723.     @Override
  724.     public int hashCode() {
  725.         return Objects.hash(_svgDocument, _size, _fitComponent, _preferredPlacement);
  726.     }

  727.     @Override
  728.     public boolean equals( Object obj ) {
  729.         if ( obj == null ) return false;
  730.         if ( obj == this ) return true;
  731.         if ( obj.getClass() != getClass() ) return false;
  732.         SvgIcon rhs = (SvgIcon) obj;
  733.         return Objects.equals(_size,               rhs._size)        &&
  734.                Objects.equals(_svgDocument,        rhs._svgDocument)  &&
  735.                Objects.equals(_fitComponent,       rhs._fitComponent) &&
  736.                Objects.equals(_preferredPlacement, rhs._preferredPlacement);
  737.     }

  738.     @Override
  739.     public String toString() {
  740.         int width  = _size.width().map(Math::round).orElse(NO_SIZE);
  741.         int height = _size.height().map(Math::round).orElse(NO_SIZE);
  742.         String typeName           = getClass().getSimpleName();
  743.         String widthAsStr              = width  < 0 ? "?" : String.valueOf(width);
  744.         String heightAsStr             = height < 0 ? "?" : String.valueOf(height);
  745.         String fitComponent       = _fitComponent.toString();
  746.         String preferredPlacement = _preferredPlacement.toString();
  747.         String svgDocument        = Optional.ofNullable(_svgDocument)
  748.                                             .map(it -> {
  749.                                                 String docClass = it.getClass().getSimpleName();
  750.                                                 FloatSize size = it.size();
  751.                                                 return docClass + "[width=" + size.width + ", height=" + size.height + "]";
  752.                                             })
  753.                                             .orElse("?");
  754.         return typeName + "[" +
  755.                     "width=" + widthAsStr + ", " +
  756.                     "height=" + heightAsStr + ", " +
  757.                     "fitComponent=" + fitComponent + ", " +
  758.                     "preferredPlacement=" + preferredPlacement + ", " +
  759.                     "doc=" + svgDocument +
  760.                 "]";
  761.     }

  762.     private Size _sizeWithAspectRatioCorrection( Size size ) {
  763.         if ( _svgDocument == null )
  764.             return size;
  765.         if ( size.hasPositiveWidth() && size.hasPositiveHeight() )
  766.             return size;
  767.         if ( size.equals(Size.unknown()) )
  768.             return size;

  769.         /*
  770.             The client code has only specified one of the dimensions
  771.             and the other dimension is unknown.

  772.             This means they want the icon to have a somewhat fixed size,
  773.             but they want the other dimension to be determined by the
  774.             aspect ratio of the SVG document.

  775.             So we try to calculate the missing dimension here:
  776.         */
  777.         return _aspectRatio().map( aspectRatio -> {
  778.                     // Now the two cases:
  779.                     if ( size.hasPositiveWidth() ) {
  780.                         // The width is known, calculate the height:
  781.                         double width = size.width().map(Number::doubleValue).orElse(1d);
  782.                         return size.withHeight((int) Math.ceil(width / aspectRatio));
  783.                     } else {
  784.                         // The height is known, calculate the width:
  785.                         double height = size.height().map(Number::doubleValue).orElse(1d);
  786.                         return size.withWidth((int) Math.ceil(height * aspectRatio));
  787.                     }
  788.                 })
  789.                 .orElse(size);
  790.     }

  791.     private Optional<Double> _aspectRatio() {
  792.         if ( _svgDocument == null )
  793.             return Optional.empty();

  794.         double aspectRatio1 = 0;
  795.         double aspectRatio2 = 0;

  796.         ViewBox viewBox = _svgDocument.viewBox();
  797.         if ( viewBox.width > 0 && viewBox.height > 0 )
  798.             aspectRatio1 = viewBox.width / viewBox.height;

  799.         FloatSize svgSize = _svgDocument.size();
  800.         if ( svgSize.width > 0 && svgSize.height > 0 )
  801.             aspectRatio2 = svgSize.width / svgSize.height;

  802.         // We prefer the "svgSize" aspect ratio over the "viewBox" aspect ratio:
  803.         double aspectRatio = aspectRatio2 > 0 ? aspectRatio2 : aspectRatio1;

  804.         if ( aspectRatio == 0 )
  805.             return Optional.empty();
  806.         else
  807.             return Optional.of(aspectRatio);
  808.     }

  809. }