| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /* $Id$ */ |
| |
| package org.apache.fop.render.intermediate; |
| |
| import java.awt.Color; |
| import java.awt.Rectangle; |
| import java.io.IOException; |
| |
| import org.apache.fop.traits.BorderProps; |
| |
| /** |
| * This is an abstract base class for handling border painting. |
| */ |
| public class BorderPainter { |
| |
| // TODO Use a class to model border instead of an array |
| /** Convention index of before top */ |
| protected static final int TOP = 0; |
| |
| /** Convention index of right border */ |
| protected static final int RIGHT = 1; |
| |
| /** Convention index of bottom border */ |
| protected static final int BOTTOM = 2; |
| |
| /** Convention index of left border */ |
| protected static final int LEFT = 3; |
| |
| // TODO Use a class to model border corners instead of an array |
| /** Convention index of top-left border corners */ |
| protected static final int TOP_LEFT = 0; |
| |
| /** Convention index of top-right-end border corners */ |
| protected static final int TOP_RIGHT = 1; |
| |
| /** Convention index of bottom-right border corners */ |
| protected static final int BOTTOM_RIGHT = 2; |
| |
| /** Convention index of bottom-left border corners */ |
| protected static final int BOTTOM_LEFT = 3; |
| |
| /** The ratio between a solid dash and the white-space in a dashed-border */ |
| public static final float DASHED_BORDER_SPACE_RATIO = 0.5f; |
| /** The length of the dash as a factor of the border width i.e. 2 -> dashWidth = 2*borderWidth */ |
| protected static final float DASHED_BORDER_LENGTH_FACTOR = 2.0f; |
| |
| private final GraphicsPainter graphicsPainter; |
| |
| public BorderPainter(GraphicsPainter graphicsPainter) { |
| this.graphicsPainter = graphicsPainter; |
| } |
| |
| /** |
| * Draws borders. |
| * @param borderRect the border rectangle |
| * @param bpsTop the border specification on the top side |
| * @param bpsBottom the border specification on the bottom side |
| * @param bpsLeft the border specification on the left side |
| * @param bpsRight the border specification on the end side |
| * @param innerBackgroundColor the inner background color |
| * @throws IFException if an error occurs while drawing the borders |
| */ |
| public void drawBorders(Rectangle borderRect, |
| BorderProps bpsTop, BorderProps bpsBottom, |
| BorderProps bpsLeft, BorderProps bpsRight, Color innerBackgroundColor) |
| throws IFException { |
| try { |
| drawRoundedBorders(borderRect, bpsTop, bpsBottom, bpsLeft, bpsRight); |
| } catch (IOException ioe) { |
| throw new IFException("IO error drawing borders", ioe); |
| } |
| } |
| |
| private BorderProps sanitizeBorderProps(BorderProps bps) { |
| return bps == null ? bps : bps.width == 0 ? (BorderProps) null : bps; |
| } |
| |
| /** |
| * TODO merge with drawRoundedBorders()? |
| * @param borderRect the border rectangle |
| * @param bpsTop the border specification on the top side |
| * @param bpsBottom the border specification on the bottom side |
| * @param bpsLeft the border specification on the left side |
| * @param bpsRight the border specification on the end side |
| * @throws IOException |
| */ |
| protected void drawRectangularBorders(Rectangle borderRect, |
| BorderProps bpsTop, BorderProps bpsBottom, |
| BorderProps bpsLeft, BorderProps bpsRight) throws IOException { |
| |
| bpsTop = sanitizeBorderProps(bpsTop); |
| bpsBottom = sanitizeBorderProps(bpsBottom); |
| bpsLeft = sanitizeBorderProps(bpsLeft); |
| bpsRight = sanitizeBorderProps(bpsRight); |
| |
| |
| int startx = borderRect.x; |
| int starty = borderRect.y; |
| int width = borderRect.width; |
| int height = borderRect.height; |
| boolean[] b = new boolean[] { |
| (bpsTop != null), (bpsRight != null), |
| (bpsBottom != null), (bpsLeft != null)}; |
| if (!b[TOP] && !b[RIGHT] && !b[BOTTOM] && !b[LEFT]) { |
| return; |
| } |
| int[] bw = new int[] { |
| (b[TOP] ? bpsTop.width : 0), |
| (b[RIGHT] ? bpsRight.width : 0), |
| (b[BOTTOM] ? bpsBottom.width : 0), |
| (b[LEFT] ? bpsLeft.width : 0)}; |
| int[] clipw = new int[] { |
| BorderProps.getClippedWidth(bpsTop), |
| BorderProps.getClippedWidth(bpsRight), |
| BorderProps.getClippedWidth(bpsBottom), |
| BorderProps.getClippedWidth(bpsLeft)}; |
| starty += clipw[TOP]; |
| height -= clipw[TOP]; |
| height -= clipw[BOTTOM]; |
| startx += clipw[LEFT]; |
| width -= clipw[LEFT]; |
| width -= clipw[RIGHT]; |
| |
| boolean[] slant = new boolean[] { |
| (b[LEFT] && b[TOP]), |
| (b[TOP] && b[RIGHT]), |
| (b[RIGHT] && b[BOTTOM]), |
| (b[BOTTOM] && b[LEFT])}; |
| if (bpsTop != null) { |
| int sx1 = startx; |
| int sx2 = (slant[TOP_LEFT] ? sx1 + bw[LEFT] - clipw[LEFT] : sx1); |
| int ex1 = startx + width; |
| int ex2 = (slant[TOP_RIGHT] ? ex1 - bw[RIGHT] + clipw[RIGHT] : ex1); |
| int outery = starty - clipw[TOP]; |
| int clipy = outery + clipw[TOP]; |
| int innery = outery + bw[TOP]; |
| |
| saveGraphicsState(); |
| moveTo(sx1, clipy); |
| |
| |
| int sx1a = sx1; |
| int ex1a = ex1; |
| if (isCollapseOuter(bpsTop)) { |
| if (isCollapseOuter(bpsLeft)) { |
| sx1a -= clipw[LEFT]; |
| } |
| if (isCollapseOuter(bpsRight)) { |
| ex1a += clipw[RIGHT]; |
| } |
| lineTo(sx1a, outery); |
| lineTo(ex1a, outery); |
| } |
| lineTo(ex1, clipy); |
| lineTo(ex2, innery); |
| lineTo(sx2, innery); |
| closePath(); |
| clip(); |
| drawBorderLine(sx1a, outery, ex1a, innery, true, true, |
| bpsTop.style, bpsTop.color); |
| restoreGraphicsState(); |
| } |
| if (bpsRight != null) { |
| int sy1 = starty; |
| int sy2 = (slant[TOP_RIGHT] ? sy1 + bw[TOP] - clipw[TOP] : sy1); |
| int ey1 = starty + height; |
| int ey2 = (slant[BOTTOM_RIGHT] ? ey1 - bw[BOTTOM] + clipw[BOTTOM] : ey1); |
| int outerx = startx + width + clipw[RIGHT]; |
| int clipx = outerx - clipw[RIGHT]; |
| int innerx = outerx - bw[RIGHT]; |
| saveGraphicsState(); |
| moveTo(clipx, sy1); |
| int sy1a = sy1; |
| int ey1a = ey1; |
| |
| if (isCollapseOuter(bpsRight)) { |
| if (isCollapseOuter(bpsTop)) { |
| sy1a -= clipw[TOP]; |
| } |
| if (isCollapseOuter(bpsBottom)) { |
| ey1a += clipw[BOTTOM]; |
| } |
| lineTo(outerx, sy1a); |
| lineTo(outerx, ey1a); |
| } |
| lineTo(clipx, ey1); |
| lineTo(innerx, ey2); |
| lineTo(innerx, sy2); |
| closePath(); |
| clip(); |
| drawBorderLine(innerx, sy1a, outerx, ey1a, false, false, |
| bpsRight.style, bpsRight.color); |
| restoreGraphicsState(); |
| } |
| if (bpsBottom != null) { |
| int sx1 = startx; |
| int sx2 = (slant[BOTTOM_LEFT] ? sx1 + bw[LEFT] - clipw[LEFT] : sx1); |
| int ex1 = startx + width; |
| int ex2 = (slant[BOTTOM_RIGHT] ? ex1 - bw[RIGHT] + clipw[RIGHT] : ex1); |
| int outery = starty + height + clipw[BOTTOM]; |
| int clipy = outery - clipw[BOTTOM]; |
| int innery = outery - bw[BOTTOM]; |
| saveGraphicsState(); |
| moveTo(ex1, clipy); |
| int sx1a = sx1; |
| int ex1a = ex1; |
| if (isCollapseOuter(bpsBottom)) { |
| if (isCollapseOuter(bpsLeft)) { |
| sx1a -= clipw[LEFT]; |
| } |
| if (isCollapseOuter(bpsRight)) { |
| ex1a += clipw[RIGHT]; |
| } |
| lineTo(ex1a, outery); |
| lineTo(sx1a, outery); |
| } |
| lineTo(sx1, clipy); |
| lineTo(sx2, innery); |
| lineTo(ex2, innery); |
| closePath(); |
| clip(); |
| drawBorderLine(sx1a, innery, ex1a, outery, true, false, |
| bpsBottom.style, bpsBottom.color); |
| restoreGraphicsState(); |
| } |
| if (bpsLeft != null) { |
| int sy1 = starty; |
| int sy2 = (slant[TOP_LEFT] ? sy1 + bw[TOP] - clipw[TOP] : sy1); |
| int ey1 = sy1 + height; |
| int ey2 = (slant[BOTTOM_LEFT] ? ey1 - bw[BOTTOM] + clipw[BOTTOM] : ey1); |
| int outerx = startx - clipw[LEFT]; |
| int clipx = outerx + clipw[LEFT]; |
| int innerx = outerx + bw[LEFT]; |
| |
| saveGraphicsState(); |
| |
| moveTo(clipx, ey1); |
| |
| int sy1a = sy1; |
| int ey1a = ey1; |
| if (isCollapseOuter(bpsLeft)) { |
| if (isCollapseOuter(bpsTop)) { |
| sy1a -= clipw[TOP]; |
| } |
| if (isCollapseOuter(bpsBottom)) { |
| ey1a += clipw[BOTTOM]; |
| } |
| lineTo(outerx, ey1a); |
| lineTo(outerx, sy1a); |
| } |
| lineTo(clipx, sy1); |
| lineTo(innerx, sy2); |
| lineTo(innerx, ey2); |
| closePath(); |
| clip(); |
| drawBorderLine(outerx, sy1a, innerx, ey1a, false, true, bpsLeft.style, bpsLeft.color); |
| restoreGraphicsState(); |
| } |
| } |
| |
| private boolean isCollapseOuter(BorderProps bp) { |
| return bp != null && bp.isCollapseOuter(); |
| } |
| |
| /** |
| * This method calculates the length of the "dash" in a dashed border. The dash satisfies the |
| * condition that corners start on a dash and end with a dash (rather than ending with a white space). |
| * @param borderLength The length of the border. |
| * @param borderWidth The width/thickness of the border. |
| * @return returns the length of the dash such that it fits the criteria above. |
| */ |
| public static float dashWidthCalculator(float borderLength, float borderWidth) { |
| float dashWidth = DASHED_BORDER_LENGTH_FACTOR * borderWidth; |
| if (borderWidth < 3) { |
| dashWidth = (DASHED_BORDER_LENGTH_FACTOR * 3) * borderWidth; |
| } |
| int period = (int) ((borderLength - dashWidth) / dashWidth / (1.0f + DASHED_BORDER_SPACE_RATIO)); |
| period = period < 0 ? 0 : period; |
| return borderLength / (period * (1.0f + DASHED_BORDER_SPACE_RATIO) + 1.0f); |
| } |
| |
| /** TODO merge with drawRectangularBorders? |
| * @param borderRect the border rectangle |
| * @throws IOException on io exception |
| * */ |
| protected void drawRoundedBorders(Rectangle borderRect, |
| BorderProps beforeBorderProps, BorderProps afterBorderProps, |
| BorderProps startBorderProps, BorderProps endBorderProps) throws IOException { |
| BorderSegment before = borderSegmentForBefore(beforeBorderProps); |
| BorderSegment after = borderSegmentForAfter(afterBorderProps); |
| BorderSegment start = borderSegmentForStart(startBorderProps); |
| BorderSegment end = borderSegmentForEnd(endBorderProps); |
| if (before.getWidth() == 0 && after.getWidth() == 0 && start.getWidth() == 0 && end.getWidth() == 0) { |
| return; |
| } |
| final int startx = borderRect.x + start.getClippedWidth(); |
| final int starty = borderRect.y + before.getClippedWidth(); |
| final int width = borderRect.width - start.getClippedWidth() - end.getClippedWidth(); |
| final int height = borderRect.height - before.getClippedWidth() - after.getClippedWidth(); |
| //Determine scale factor if any adjacent elliptic corners overlap |
| double cornerCorrectionFactor = calculateCornerScaleCorrection(width, height, before, after, start, |
| end); |
| drawBorderSegment(start, before, end, 0, width, startx, starty, cornerCorrectionFactor); |
| drawBorderSegment(before, end, after, 1, height, startx + width, starty, cornerCorrectionFactor); |
| drawBorderSegment(end, after, start, 2, width, startx + width, starty + height, |
| cornerCorrectionFactor); |
| drawBorderSegment(after, start, before, 3, height, startx, starty + height, cornerCorrectionFactor); |
| } |
| |
| private void drawBorderSegment(BorderSegment start, BorderSegment before, BorderSegment end, |
| int orientation, int width, int x, int y, double cornerCorrectionFactor) throws IOException { |
| if (before.getWidth() != 0) { |
| //Let x increase in the START->END direction |
| final int sx2 = start.getWidth() - start.getClippedWidth(); |
| final int ex1 = width; |
| final int ex2 = ex1 - end.getWidth() + end.getClippedWidth(); |
| final int outery = -before.getClippedWidth(); |
| final int innery = outery + before.getWidth(); |
| final int ellipseSBRadiusX = correctRadius(cornerCorrectionFactor, start.getRadiusEnd()); |
| final int ellipseSBRadiusY = correctRadius(cornerCorrectionFactor, before.getRadiusStart()); |
| final int ellipseBERadiusX = correctRadius(cornerCorrectionFactor, end.getRadiusStart()); |
| final int ellipseBERadiusY = correctRadius(cornerCorrectionFactor, before.getRadiusEnd()); |
| saveGraphicsState(); |
| translateCoordinates(x, y); |
| if (orientation != 0) { |
| rotateCoordinates(Math.PI * orientation / 2d); |
| } |
| final int ellipseSBX = ellipseSBRadiusX; |
| final int ellipseSBY = ellipseSBRadiusY; |
| final int ellipseBEX = ex1 - ellipseBERadiusX; |
| final int ellipseBEY = ellipseBERadiusY; |
| int sx1a = 0; |
| int ex1a = ex1; |
| if (ellipseSBRadiusX != 0 && ellipseSBRadiusY != 0) { |
| final double[] joinMetrics = getCornerBorderJoinMetrics(ellipseSBRadiusX, |
| ellipseSBRadiusY, sx2, innery); |
| final double outerJoinPointX = joinMetrics[0]; |
| final double outerJoinPointY = joinMetrics[1]; |
| final double sbJoinAngle = joinMetrics[2]; |
| moveTo((int) outerJoinPointX, (int) outerJoinPointY); |
| arcTo(Math.PI + sbJoinAngle, Math.PI * 3 / 2, |
| ellipseSBX, ellipseSBY, ellipseSBRadiusX, ellipseSBRadiusY); |
| } else { |
| moveTo(0, 0); |
| if (before.isCollapseOuter()) { |
| if (start.isCollapseOuter()) { |
| sx1a -= start.getClippedWidth(); |
| } |
| if (end.isCollapseOuter()) { |
| ex1a += end.getClippedWidth(); |
| } |
| lineTo(sx1a, outery); |
| lineTo(ex1a, outery); |
| } |
| } |
| if (ellipseBERadiusX != 0 && ellipseBERadiusY != 0) { |
| final double[] outerJoinMetrics = getCornerBorderJoinMetrics( |
| ellipseBERadiusX, ellipseBERadiusY, ex1 - ex2, innery); |
| final double beJoinAngle = ex1 == ex2 ? Math.PI / 2 : Math.PI / 2 - outerJoinMetrics[2]; |
| lineTo(ellipseBEX, 0); |
| arcTo(Math.PI * 3 / 2 , Math.PI * 3 / 2 + beJoinAngle, |
| ellipseBEX, ellipseBEY, ellipseBERadiusX, ellipseBERadiusY); |
| if (ellipseBEX < ex2 && ellipseBEY > innery) { |
| final double[] innerJoinMetrics = getCornerBorderJoinMetrics( |
| (double) ex2 - ellipseBEX, (double) ellipseBEY - innery, ex1 - ex2, innery); |
| final double innerJoinPointX = innerJoinMetrics[0]; |
| final double innerJoinPointY = innerJoinMetrics[1]; |
| final double beInnerJoinAngle = Math.PI / 2 - innerJoinMetrics[2]; |
| lineTo((int) (ex2 - innerJoinPointX), (int) (innerJoinPointY + innery)); |
| arcTo(beInnerJoinAngle + Math.PI * 3 / 2, Math.PI * 3 / 2, |
| ellipseBEX, ellipseBEY, ex2 - ellipseBEX, ellipseBEY - innery); |
| } else { |
| lineTo(ex2, innery); |
| } |
| } else { |
| lineTo(ex1, 0); |
| lineTo(ex2, innery); |
| } |
| if (ellipseSBRadiusX == 0) { |
| lineTo(sx2, innery); |
| } else { |
| if (ellipseSBX > sx2 && ellipseSBY > innery) { |
| final double[] innerJoinMetrics = getCornerBorderJoinMetrics(ellipseSBRadiusX - sx2, |
| ellipseSBRadiusY - innery, sx2, innery); |
| final double sbInnerJoinAngle = innerJoinMetrics[2]; |
| lineTo(ellipseSBX, innery); |
| arcTo(Math.PI * 3 / 2, sbInnerJoinAngle + Math.PI, |
| ellipseSBX, ellipseSBY, ellipseSBX - sx2, ellipseSBY - innery); |
| } else { |
| lineTo(sx2, innery); |
| } |
| } |
| closePath(); |
| clip(); |
| if (ellipseBERadiusY == 0 && ellipseSBRadiusY == 0) { |
| drawBorderLine(sx1a, outery, ex1a, innery, true, true, |
| before.getStyle(), before.getColor()); |
| } else { |
| int innerFillY = Math.max(Math.max(ellipseBEY, ellipseSBY), innery); |
| drawBorderLine(sx1a, outery, ex1a, innerFillY, true, true, |
| before.getStyle(), before.getColor()); |
| } |
| restoreGraphicsState(); |
| } |
| } |
| |
| private static int correctRadius(double cornerCorrectionFactor, int radius) { |
| return (int) (Math.round(cornerCorrectionFactor * radius)); |
| } |
| |
| private static BorderSegment borderSegmentForBefore(BorderProps before) { |
| return AbstractBorderSegment.asBorderSegment(before); |
| } |
| |
| private static BorderSegment borderSegmentForAfter(BorderProps after) { |
| return AbstractBorderSegment.asFlippedBorderSegment(after); |
| } |
| |
| private static BorderSegment borderSegmentForStart(BorderProps start) { |
| return AbstractBorderSegment.asFlippedBorderSegment(start); |
| } |
| |
| private static BorderSegment borderSegmentForEnd(BorderProps end) { |
| return AbstractBorderSegment.asBorderSegment(end); |
| } |
| |
| private interface BorderSegment { |
| |
| Color getColor(); |
| |
| int getStyle(); |
| |
| int getWidth(); |
| |
| int getClippedWidth(); |
| |
| int getRadiusStart(); |
| |
| int getRadiusEnd(); |
| |
| boolean isCollapseOuter(); |
| |
| boolean isSpecified(); |
| } |
| |
| private abstract static class AbstractBorderSegment implements BorderSegment { |
| |
| private static BorderSegment asBorderSegment(BorderProps borderProps) { |
| return borderProps == null ? NullBorderSegment.INSTANCE : new WrappingBorderSegment(borderProps); |
| } |
| |
| private static BorderSegment asFlippedBorderSegment(BorderProps borderProps) { |
| return borderProps == null ? NullBorderSegment.INSTANCE : new FlippedBorderSegment(borderProps); |
| } |
| |
| public boolean isSpecified() { |
| return !(this instanceof NullBorderSegment); |
| } |
| |
| private static class WrappingBorderSegment extends AbstractBorderSegment { |
| |
| protected final BorderProps borderProps; |
| |
| private final int clippedWidth; |
| |
| WrappingBorderSegment(BorderProps borderProps) { |
| this.borderProps = borderProps; |
| clippedWidth = BorderProps.getClippedWidth(borderProps); |
| } |
| |
| public int getStyle() { |
| return borderProps.style; |
| } |
| |
| public Color getColor() { |
| return borderProps.color; |
| } |
| |
| public int getWidth() { |
| return borderProps.width; |
| } |
| |
| public int getClippedWidth() { |
| return clippedWidth; |
| } |
| public boolean isCollapseOuter() { |
| return borderProps.isCollapseOuter(); |
| } |
| |
| public int getRadiusStart() { |
| return borderProps.getRadiusStart(); |
| } |
| |
| public int getRadiusEnd() { |
| return borderProps.getRadiusEnd(); |
| } |
| } |
| |
| private static class FlippedBorderSegment extends WrappingBorderSegment { |
| |
| FlippedBorderSegment(BorderProps borderProps) { |
| super(borderProps); |
| } |
| |
| public int getRadiusStart() { |
| return borderProps.getRadiusEnd(); |
| } |
| |
| public int getRadiusEnd() { |
| return borderProps.getRadiusStart(); |
| } |
| } |
| |
| private static final class NullBorderSegment extends AbstractBorderSegment { |
| |
| public static final NullBorderSegment INSTANCE = new NullBorderSegment(); |
| |
| private NullBorderSegment() { |
| } |
| |
| public int getWidth() { |
| return 0; |
| } |
| |
| public int getClippedWidth() { |
| return 0; |
| } |
| |
| public int getRadiusStart() { |
| return 0; |
| } |
| |
| public int getRadiusEnd() { |
| return 0; |
| } |
| |
| public boolean isCollapseOuter() { |
| return false; |
| } |
| |
| public Color getColor() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public int getStyle() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public boolean isSpecified() { |
| return false; |
| } |
| } |
| } |
| |
| private double[] getCornerBorderJoinMetrics(double ellipseCenterX, double ellipseCenterY, double xWidth, |
| double yWidth) { |
| if (xWidth > 0) { |
| return getCornerBorderJoinMetrics(ellipseCenterX, ellipseCenterY, yWidth / xWidth); |
| } else { |
| return new double[]{0, ellipseCenterY, 0}; |
| } |
| } |
| |
| private double[] getCornerBorderJoinMetrics(double ellipseCenterX, double ellipseCenterY, |
| double borderWidthRatio) { |
| double x = ellipseCenterY * ellipseCenterX * ( |
| ellipseCenterY + ellipseCenterX * borderWidthRatio |
| - Math.sqrt(2d * ellipseCenterX * ellipseCenterY * borderWidthRatio) |
| ) / (ellipseCenterY * ellipseCenterY |
| + ellipseCenterX * ellipseCenterX * borderWidthRatio * borderWidthRatio); |
| double y = borderWidthRatio * x; |
| return new double[]{x, y, Math.atan((ellipseCenterY - y) / (ellipseCenterX - x))}; |
| } |
| |
| /** |
| * Clip the background to the inner border |
| * @param rect clipping rectangle |
| * @param bpsBefore before border |
| * @param bpsAfter after border |
| * @param bpsStart start border |
| * @param bpsEnd end border |
| * @throws IOException if an I/O error occurs |
| */ |
| public void clipBackground(Rectangle rect, |
| BorderProps bpsBefore, BorderProps bpsAfter, |
| BorderProps bpsStart, BorderProps bpsEnd) throws IOException { |
| BorderSegment before = borderSegmentForBefore(bpsBefore); |
| BorderSegment after = borderSegmentForAfter(bpsAfter); |
| BorderSegment start = borderSegmentForStart(bpsStart); |
| BorderSegment end = borderSegmentForEnd(bpsEnd); |
| int startx = rect.x; |
| int starty = rect.y; |
| int width = rect.width; |
| int height = rect.height; |
| double correctionFactor = calculateCornerCorrectionFactor(width + start.getWidth() + end.getWidth(), |
| height + before.getWidth() + after.getWidth(), bpsBefore, bpsAfter, bpsStart, bpsEnd); |
| Corner cornerBeforeEnd = Corner.createBeforeEndCorner(before, end, correctionFactor); |
| Corner cornerEndAfter = Corner.createEndAfterCorner(end, after, correctionFactor); |
| Corner cornerAfterStart = Corner.createAfterStartCorner(after, start, correctionFactor); |
| Corner cornerStartBefore = Corner.createStartBeforeCorner(start, before, correctionFactor); |
| new PathPainter(startx + cornerStartBefore.radiusX, starty) |
| .lineHorizTo(width - cornerStartBefore.radiusX - cornerBeforeEnd.radiusX) |
| .drawCorner(cornerBeforeEnd) |
| .lineVertTo(height - cornerBeforeEnd.radiusY - cornerEndAfter.radiusY) |
| .drawCorner(cornerEndAfter) |
| .lineHorizTo(cornerEndAfter.radiusX + cornerAfterStart.radiusX - width) |
| .drawCorner(cornerAfterStart) |
| .lineVertTo(cornerAfterStart.radiusY + cornerStartBefore.radiusY - height) |
| .drawCorner(cornerStartBefore); |
| clip(); |
| } |
| |
| |
| |
| /** |
| * The four corners |
| * SB - Start-Before |
| * BE - Before-End |
| * EA - End-After |
| * AS - After-Start |
| * |
| * 0 --> x |
| * | |
| * v |
| * y |
| * |
| * SB BE |
| * *----* |
| * | | |
| * | | |
| * *----* |
| * AS EA |
| * |
| */ |
| private enum CornerAngles { |
| /** The before-end angles */ |
| BEFORE_END(Math.PI * 3 / 2, 0), |
| /** The end-after angles */ |
| END_AFTER(0, Math.PI / 2), |
| /** The after-start angles*/ |
| AFTER_START(Math.PI / 2, Math.PI), |
| /** The start-before angles */ |
| START_BEFORE(Math.PI, Math.PI * 3 / 2); |
| |
| /** Angle of the start of the corner arch relative to the x-axis in the counter-clockwise direction */ |
| private final double start; |
| |
| /** Angle of the end of the corner arch relative to the x-axis in the counter-clockwise direction */ |
| private final double end; |
| |
| CornerAngles(double start, double end) { |
| this.start = start; |
| this.end = end; |
| } |
| |
| } |
| |
| private static final class Corner { |
| |
| private static final Corner SQUARE = new Corner(0, 0, null, 0, 0, 0, 0); |
| |
| /** The radius of the elliptic corner in the x direction */ |
| private final int radiusX; |
| |
| /** The radius of the elliptic corner in the y direction */ |
| private final int radiusY; |
| |
| /** The start and end angles of the corner ellipse */ |
| private final CornerAngles angles; |
| |
| /** The offset in the x direction of the center of the ellipse relative to the starting point */ |
| private final int centerX; |
| |
| /** The offset in the y direction of the center of the ellipse relative to the starting point */ |
| private final int centerY; |
| |
| /** The value in the x direction that the corner extends relative to the starting point */ |
| private final int incrementX; |
| |
| /** The value in the y direction that the corner extends relative to the starting point */ |
| private final int incrementY; |
| |
| private Corner(int radiusX, int radiusY, CornerAngles angles, int ellipseOffsetX, |
| int ellipseOffsetY, int incrementX, int incrementY) { |
| this.radiusX = radiusX; |
| this.radiusY = radiusY; |
| this.angles = angles; |
| this.centerX = ellipseOffsetX; |
| this.centerY = ellipseOffsetY; |
| this.incrementX = incrementX; |
| this.incrementY = incrementY; |
| } |
| |
| private static int extentFromRadiusStart(BorderSegment border, double correctionFactor) { |
| return extentFromRadius(border.getRadiusStart(), border, correctionFactor); |
| } |
| |
| private static int extentFromRadiusEnd(BorderSegment border, double correctionFactor) { |
| return extentFromRadius(border.getRadiusEnd(), border, correctionFactor); |
| } |
| |
| private static int extentFromRadius(int radius, BorderSegment border, double correctionFactor) { |
| return Math.max((int) (radius * correctionFactor) - border.getWidth(), 0); |
| } |
| |
| public static Corner createBeforeEndCorner(BorderSegment before, BorderSegment end, |
| double correctionFactor) { |
| int width = end.getRadiusStart(); |
| int height = before.getRadiusEnd(); |
| if (width == 0 || height == 0) { |
| return SQUARE; |
| } |
| int x = extentFromRadiusStart(end, correctionFactor); |
| int y = extentFromRadiusEnd(before, correctionFactor); |
| return new Corner(x, y, CornerAngles.BEFORE_END, 0, y, x, y); |
| } |
| |
| public static Corner createEndAfterCorner(BorderSegment end, BorderSegment after, |
| double correctionFactor) { |
| int width = end.getRadiusEnd(); |
| int height = after.getRadiusStart(); |
| if (width == 0 || height == 0) { |
| return SQUARE; |
| } |
| int x = extentFromRadiusEnd(end, correctionFactor); |
| int y = extentFromRadiusStart(after, correctionFactor); |
| return new Corner(x, y, CornerAngles.END_AFTER, -x, 0, -x, y); |
| } |
| |
| public static Corner createAfterStartCorner(BorderSegment after, BorderSegment start, |
| double correctionFactor) { |
| int width = start.getRadiusStart(); |
| int height = after.getRadiusEnd(); |
| if (width == 0 || height == 0) { |
| return SQUARE; |
| } |
| int x = extentFromRadiusStart(start, correctionFactor); |
| int y = extentFromRadiusEnd(after, correctionFactor); |
| return new Corner(x, y, CornerAngles.AFTER_START, 0, -y, -x, -y); |
| } |
| |
| public static Corner createStartBeforeCorner(BorderSegment start, BorderSegment before, |
| double correctionFactor) { |
| int width = start.getRadiusEnd(); |
| int height = before.getRadiusStart(); |
| if (width == 0 || height == 0) { |
| return SQUARE; |
| } |
| int x = extentFromRadiusEnd(start, correctionFactor); |
| int y = extentFromRadiusStart(before, correctionFactor); |
| return new Corner(x, y, CornerAngles.START_BEFORE, x, 0, x, -y); |
| } |
| } |
| |
| /** |
| * This is a helper class for constructing curves composed of move, line and arc operations. Coordinates |
| * are relative to the terminal point of the previous operation |
| */ |
| private final class PathPainter { |
| |
| /** Current x position */ |
| private int x; |
| |
| /** Current y position */ |
| private int y; |
| |
| PathPainter(int x, int y) throws IOException { |
| moveTo(x, y); |
| } |
| |
| private void moveTo(int x, int y) throws IOException { |
| this.x += x; |
| this.y += y; |
| BorderPainter.this.moveTo(this.x, this.y); |
| } |
| |
| public PathPainter lineTo(int x, int y) throws IOException { |
| this.x += x; |
| this.y += y; |
| BorderPainter.this.lineTo(this.x, this.y); |
| return this; |
| } |
| |
| public PathPainter lineHorizTo(int x) throws IOException { |
| return lineTo(x, 0); |
| } |
| |
| public PathPainter lineVertTo(int y) throws IOException { |
| return lineTo(0, y); |
| } |
| |
| PathPainter drawCorner(Corner corner) throws IOException { |
| if (corner.radiusX == 0 && corner.radiusY == 0) { |
| return this; |
| } |
| if (corner.radiusX == 0 || corner.radiusY == 0) { |
| x += corner.incrementX; |
| y += corner.incrementY; |
| BorderPainter.this.lineTo(x, y); |
| return this; |
| } |
| BorderPainter.this.arcTo(corner.angles.start, corner.angles.end, x + corner.centerX, |
| y + corner.centerY, corner.radiusX, corner.radiusY); |
| x += corner.incrementX; |
| y += corner.incrementY; |
| return this; |
| } |
| } |
| |
| /** |
| * Calculate the correction factor to handle over-sized elliptic corner radii. |
| * |
| * @param width the border width |
| * @param height the border height |
| * @param before the before border properties |
| * @param after the after border properties |
| * @param start the start border properties |
| * @param end the end border properties |
| * |
| */ |
| protected static double calculateCornerCorrectionFactor(int width, int height, BorderProps before, |
| BorderProps after, BorderProps start, BorderProps end) { |
| return calculateCornerScaleCorrection(width, height, borderSegmentForBefore(before), |
| borderSegmentForAfter(after), borderSegmentForStart(start), borderSegmentForEnd(end)); |
| } |
| |
| /** |
| * Calculate the scaling factor to handle over-sized elliptic corner radii. |
| * |
| * @param width the border width |
| * @param height the border height |
| * @param before the before border segment |
| * @param after the after border segment |
| * @param start the start border segment |
| * @param end the end border segment |
| */ |
| protected static double calculateCornerScaleCorrection(int width, int height, BorderSegment before, |
| BorderSegment after, BorderSegment start, BorderSegment end) { |
| return CornerScaleCorrectionCalculator.calculate(width, height, before, after, start, end); |
| } |
| |
| private static final class CornerScaleCorrectionCalculator { |
| |
| private double correctionFactor = 1; |
| |
| private CornerScaleCorrectionCalculator(int width, int height, |
| BorderSegment before, BorderSegment after, |
| BorderSegment start, BorderSegment end) { |
| calculateForSegment(width, start, before, end); |
| calculateForSegment(height, before, end, after); |
| calculateForSegment(width, end, after, start); |
| calculateForSegment(height, after, start, before); |
| } |
| |
| public static double calculate(int width, int height, |
| BorderSegment before, BorderSegment after, |
| BorderSegment start, BorderSegment end) { |
| return new CornerScaleCorrectionCalculator(width, height, before, after, start, end) |
| .correctionFactor; |
| } |
| |
| private void calculateForSegment(int width, BorderSegment bpsStart, BorderSegment bpsBefore, |
| BorderSegment bpsEnd) { |
| if (bpsBefore.isSpecified()) { |
| double ellipseExtent = bpsStart.getRadiusEnd() + bpsEnd.getRadiusStart(); |
| if (ellipseExtent > 0) { |
| double thisCorrectionFactor = width / ellipseExtent; |
| if (thisCorrectionFactor < correctionFactor) { |
| correctionFactor = thisCorrectionFactor; |
| } |
| } |
| } |
| } |
| } |
| |
| private void drawBorderLine(int x1, int y1, int x2, int y2, boolean horz, boolean startOrBefore, |
| int style, Color color) throws IOException { |
| graphicsPainter.drawBorderLine(x1, y1, x2, y2, horz, startOrBefore, style, color); |
| } |
| |
| private void moveTo(int x, int y) throws IOException { |
| graphicsPainter.moveTo(x, y); |
| } |
| |
| private void lineTo(int x, int y) throws IOException { |
| graphicsPainter.lineTo(x, y); |
| } |
| |
| private void arcTo(final double startAngle, final double endAngle, final int cx, final int cy, |
| final int width, final int height) throws IOException { |
| graphicsPainter.arcTo(startAngle, endAngle, cx, cy, width, height); |
| } |
| |
| private void rotateCoordinates(double angle) throws IOException { |
| graphicsPainter.rotateCoordinates(angle); |
| } |
| |
| private void translateCoordinates(int xTranslate, int yTranslate) throws IOException { |
| graphicsPainter.translateCoordinates(xTranslate, yTranslate); |
| } |
| |
| private void closePath() throws IOException { |
| graphicsPainter.closePath(); |
| } |
| |
| private void clip() throws IOException { |
| graphicsPainter.clip(); |
| } |
| |
| private void saveGraphicsState() throws IOException { |
| graphicsPainter.saveGraphicsState(); |
| } |
| |
| private void restoreGraphicsState() throws IOException { |
| graphicsPainter.restoreGraphicsState(); |
| } |
| |
| } |