Need a way to scale a font to fit a rectangle

前端 未结 6 1216
耶瑟儿~
耶瑟儿~ 2021-01-01 19:46

I just wrote some code to scale a font to fit within (the length of) a rectangle. It starts at 18 width and iterates down until it fits.

This seems horribly ineffic

相关标签:
6条回答
  • 2021-01-01 20:32

    Change all width variables to float instead of int for better result.

    public static Font scaleFontToFit(String text, int width, Graphics g, Font pFont)
    {
        float fontSize = pFont.getSize();
        float fWidth = g.getFontMetrics(pFont).stringWidth(text);
        if(fWidth <= width)
            return pFont;
        fontSize = ((float)width / fWidth) * fontSize;
        return pFont.deriveFont(fontSize);
    }
    
    0 讨论(0)
  • 2021-01-01 20:33

    You could use interpolation search:

    public static Font scaleFont(String text, Rectangle rect, Graphics g, Font pFont) {
        float min=0.1f;
        float max=72f;
        float size=18.0f;
        Font font=pFont;
    
        while(max - min <= 0.1) {
            font = g.getFont().deriveFont(size);
            FontMetrics fm = g.getFontMetrics(font);
            int width = fm.stringWidth(text);
            if (width == rect.width) {
                return font;
            } else {
                if (width < rect.width) {
                    min = size;
                } else {
                    max = size;
                }
                size = Math.min(max, Math.max(min, size * (float)rect.width / (float)width));
            }
        }
        return font;
    }
    
    0 讨论(0)
  • Semi-pseudo code:

    public Font scaleFont(
        String text, Rectangle rect, Graphics g, Font font) {
        float fontSize = 20.0f;
    
        font = g.getFont().deriveFont(fontSize);
        int width = g.getFontMetrics(font).stringWidth(text);
        fontSize = (rect.width / width ) * fontSize;
        return g.getFont().deriveFont(fontSize);
    }
    

    A derivation that iterates:

    /**
     * Adjusts the given {@link Font}/{@link String} size such that it fits
     * within the bounds of the given {@link Rectangle}.
     *
     * @param label    Contains the text and font to scale.
     * @param dst      The bounds for fitting the string.
     * @param graphics The context for rendering the string.
     * @return A new {@link Font} instance that is guaranteed to write the given
     * string within the bounds of the given {@link Rectangle}.
     */
    public Font scaleFont(
        final JLabel label, final Rectangle dst, final Graphics graphics ) {
      assert label != null;
      assert dst != null;
      assert graphics != null;
    
      final var font = label.getFont();
      final var text = label.getText();
    
      final var frc = ((Graphics2D) graphics).getFontRenderContext();
    
      final var dstWidthPx = dst.getWidth();
      final var dstHeightPx = dst.getHeight();
    
      var minSizePt = 1f;
      var maxSizePt = 1000f;
      var scaledFont = font;
      float scaledPt = scaledFont.getSize();
    
      while( maxSizePt - minSizePt > 1f ) {
        scaledFont = scaledFont.deriveFont( scaledPt );
    
        final var layout = new TextLayout( text, scaledFont, frc );
        final var fontWidthPx = layout.getVisibleAdvance();
    
        final var metrics = scaledFont.getLineMetrics( text, frc );
        final var fontHeightPx = metrics.getHeight();
    
        if( (fontWidthPx > dstWidthPx) || (fontHeightPx > dstHeightPx) ) {
          maxSizePt = scaledPt;
        }
        else {
          minSizePt = scaledPt;
        }
    
        scaledPt = (minSizePt + maxSizePt) / 2;
      }
    
      return scaledFont.deriveFont( (float) Math.floor( scaledPt ) );
    }
    

    Imagine you want to add a label to a component that has rectangular bounds r such that the label completely fills the component's area. One could write:

    final Font DEFAULT_FONT = new Font( "DejaVu Sans", BOLD, 12 );
    final Color COLOUR_LABEL = new Color( 33, 33, 33 );
    
    // TODO: Return a valid container component instance.
    final var r = getComponent().getBounds();
    final var graphics = getComponent().getGraphics();
    
    final int width = (int) r.getWidth();
    final int height = (int) r.getHeight();
    
    final var label = new JLabel( text );
    label.setFont( DEFAULT_FONT );
    label.setSize( width, height );
    label.setForeground( COLOUR_LABEL );
    
    final var scaledFont = scaleFont( label, r, graphics );
    label.setFont( scaledFont );
    
    0 讨论(0)
  • 2021-01-01 20:37

    You can improve the efficiency using a binary search pattern - high/low with some granularity - either 1, 0.5 or 0.25 points.

    For example, guess at 18, too high? Move to 9, too Low? 13.5, too low? 15.75, too high? 14!

    0 讨论(0)
  • 2021-01-01 20:43

    A different, obvious way would be to have the text pre-drawn on a bitmap, and then shrink the bitmap to fit the rectangle; but, because of hand-crafted font design and hinting etc., finding the right font size produces the best-looking (although perhaps not the quickest) result.

    0 讨论(0)
  • 2021-01-01 20:51
    private Font scaleFont ( String text, Rectangle rect, Graphics gc )
    {
        final float fMinimumFont = 0.1f;
        float fMaximumFont = 1000f;
    
        /* Use Point2d.Float to hold ( font, width of font in pixels ) pairs. */
        Point2D.Float lowerPoint = new Point2D.Float ( fMinimumFont, getWidthInPixelsOfString ( text, fMinimumFont, gc ) );
        Point2D.Float upperPoint = new Point2D.Float ( fMaximumFont, getWidthInPixelsOfString ( text, fMaximumFont, gc ) );
        Point2D.Float midPoint = new Point2D.Float ();
    
        for ( int i = 0; i < 50; i++ )
        {
            float middleFont = ( lowerPoint.x + upperPoint.x ) / 2;
    
            midPoint.setLocation ( middleFont, getWidthInPixelsOfString ( text, middleFont, gc ) );
    
            if ( midPoint.y >= rect.getWidth () * .95 && midPoint.y <= rect.getWidth () )
                break;
            else if ( midPoint.y < rect.getWidth () )
                lowerPoint.setLocation ( midPoint );
            else if ( midPoint.y > rect.getWidth () )
                upperPoint.setLocation ( midPoint );
        }
    
        fMaximumFont = midPoint.x;
    
        Font font = gc.getFont ().deriveFont ( fMaximumFont );
    
        /* Now use Point2d.Float to hold ( font, height of font in pixels ) pairs. */
        lowerPoint.setLocation ( fMinimumFont, getHeightInPixelsOfString ( text, fMinimumFont, gc ) );
        upperPoint.setLocation ( fMaximumFont, getHeightInPixelsOfString ( text, fMaximumFont, gc ) );
    
        if ( upperPoint.y < rect.getHeight () )
            return font;
    
        for ( int i = 0; i < 50; i++ )
        {
            float middleFont = ( lowerPoint.x + upperPoint.x ) / 2;
    
            midPoint.setLocation ( middleFont, getHeightInPixelsOfString ( text, middleFont, gc ) );
    
            if ( midPoint.y >= rect.getHeight () * .95 && midPoint.y <= rect.getHeight () )
                break;
            else if ( midPoint.y < rect.getHeight () )
                lowerPoint.setLocation ( midPoint );
            else if ( midPoint.y > rect.getHeight () )
                upperPoint.setLocation ( midPoint );
        }
    
        fMaximumFont = midPoint.x;
    
        font = gc.getFont ().deriveFont ( fMaximumFont );
    
        return font;
    }
    
    
    private float getWidthInPixelsOfString ( String str, float fontSize, Graphics gc )
    {
        Font font = gc.getFont ().deriveFont ( fontSize );
    
        return getWidthInPixelsOfString ( str, font, gc );
    }
    
    private float getWidthInPixelsOfString ( String str, Font font, Graphics gc )
    {
        FontMetrics fm = gc.getFontMetrics ( font );
        int nWidthInPixelsOfCurrentFont = fm.stringWidth ( str );
    
        return (float) nWidthInPixelsOfCurrentFont;
    }
    
    
    private float getHeightInPixelsOfString ( String string, float fontSize, Graphics gc )
    {
        Font font = gc.getFont ().deriveFont ( fontSize );
    
        return getHeightInPixelsOfString ( string, font, gc );
    }
    
    private float getHeightInPixelsOfString ( String string, Font font, Graphics gc )
    {
        FontMetrics metrics = gc.getFontMetrics ( font );
        int nHeightInPixelsOfCurrentFont = (int) metrics.getStringBounds ( string, gc ).getHeight () - metrics.getDescent () - metrics.getLeading ();
    
        return (float) nHeightInPixelsOfCurrentFont * .75f;
    }
    
    0 讨论(0)
提交回复
热议问题