488 lines
13 KiB
Python
Executable File
488 lines
13 KiB
Python
Executable File
# Copyright (c) 2010-2014 openpyxl
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
# THE SOFTWARE.
|
|
#
|
|
# @license: http://www.opensource.org/licenses/mit-license.php
|
|
# @author: see AUTHORS file
|
|
|
|
import math
|
|
from openpyxl.style import Color
|
|
from openpyxl.shared.units import pixels_to_EMU, EMU_to_pixels, short_color
|
|
from openpyxl.cell import column_index_from_string
|
|
|
|
class Shadow(object):
|
|
|
|
SHADOW_BOTTOM = 'b'
|
|
SHADOW_BOTTOM_LEFT = 'bl'
|
|
SHADOW_BOTTOM_RIGHT = 'br'
|
|
SHADOW_CENTER = 'ctr'
|
|
SHADOW_LEFT = 'l'
|
|
SHADOW_TOP = 't'
|
|
SHADOW_TOP_LEFT = 'tl'
|
|
SHADOW_TOP_RIGHT = 'tr'
|
|
|
|
def __init__(self):
|
|
self.visible = False
|
|
self.blurRadius = 6
|
|
self.distance = 2
|
|
self.direction = 0
|
|
self.alignment = self.SHADOW_BOTTOM_RIGHT
|
|
self.color = Color(Color.BLACK)
|
|
self.alpha = 50
|
|
|
|
|
|
class Drawing(object):
|
|
""" a drawing object - eg container for shapes or charts
|
|
we assume user specifies dimensions in pixels; units are
|
|
converted to EMU in the drawing part
|
|
"""
|
|
|
|
count = 0
|
|
|
|
def __init__(self):
|
|
|
|
self.name = ''
|
|
self.description = ''
|
|
self.coordinates = ((1, 2), (16, 8))
|
|
self.left = 0
|
|
self.top = 0
|
|
self._width = EMU_to_pixels(200000)
|
|
self._height = EMU_to_pixels(1828800)
|
|
self.resize_proportional = False
|
|
self.rotation = 0
|
|
# self.shadow = Shadow()
|
|
|
|
@property
|
|
def width(self):
|
|
return self._width
|
|
|
|
@width.setter
|
|
def width(self, w):
|
|
if self.resize_proportional and w:
|
|
ratio = self._height / self._width
|
|
self._height = round(ratio * w)
|
|
self._width = w
|
|
|
|
@property
|
|
def height(self):
|
|
return self._height
|
|
|
|
@height.setter
|
|
def height(self, h):
|
|
if self.resize_proportional and h:
|
|
ratio = self._width / self._height
|
|
self._width = round(ratio * h)
|
|
self._height = h
|
|
|
|
def set_dimension(self, w=0, h=0):
|
|
|
|
xratio = w / self._width
|
|
yratio = h / self._height
|
|
|
|
if self.resize_proportional and w and h:
|
|
if (xratio * self._height) < h:
|
|
self._height = math.ceil(xratio * self._height)
|
|
self._width = w
|
|
else:
|
|
self._width = math.ceil(yratio * self._width)
|
|
self._height = h
|
|
|
|
def get_emu_dimensions(self):
|
|
""" return (x, y, w, h) in EMU """
|
|
|
|
return (pixels_to_EMU(self.left), pixels_to_EMU(self.top),
|
|
pixels_to_EMU(self._width), pixels_to_EMU(self._height))
|
|
|
|
|
|
class Shape(object):
|
|
""" a drawing inside a chart
|
|
coordiantes are specified by the user in the axis units
|
|
"""
|
|
|
|
MARGIN_LEFT = 6 + 13 + 1
|
|
MARGIN_BOTTOM = 17 + 11
|
|
|
|
FONT_WIDTH = 7
|
|
FONT_HEIGHT = 8
|
|
|
|
ROUND_RECT = 'roundRect'
|
|
RECT = 'rect'
|
|
|
|
# other shapes to define :
|
|
'''
|
|
"line"
|
|
"lineInv"
|
|
"triangle"
|
|
"rtTriangle"
|
|
"diamond"
|
|
"parallelogram"
|
|
"trapezoid"
|
|
"nonIsoscelesTrapezoid"
|
|
"pentagon"
|
|
"hexagon"
|
|
"heptagon"
|
|
"octagon"
|
|
"decagon"
|
|
"dodecagon"
|
|
"star4"
|
|
"star5"
|
|
"star6"
|
|
"star7"
|
|
"star8"
|
|
"star10"
|
|
"star12"
|
|
"star16"
|
|
"star24"
|
|
"star32"
|
|
"roundRect"
|
|
"round1Rect"
|
|
"round2SameRect"
|
|
"round2DiagRect"
|
|
"snipRoundRect"
|
|
"snip1Rect"
|
|
"snip2SameRect"
|
|
"snip2DiagRect"
|
|
"plaque"
|
|
"ellipse"
|
|
"teardrop"
|
|
"homePlate"
|
|
"chevron"
|
|
"pieWedge"
|
|
"pie"
|
|
"blockArc"
|
|
"donut"
|
|
"noSmoking"
|
|
"rightArrow"
|
|
"leftArrow"
|
|
"upArrow"
|
|
"downArrow"
|
|
"stripedRightArrow"
|
|
"notchedRightArrow"
|
|
"bentUpArrow"
|
|
"leftRightArrow"
|
|
"upDownArrow"
|
|
"leftUpArrow"
|
|
"leftRightUpArrow"
|
|
"quadArrow"
|
|
"leftArrowCallout"
|
|
"rightArrowCallout"
|
|
"upArrowCallout"
|
|
"downArrowCallout"
|
|
"leftRightArrowCallout"
|
|
"upDownArrowCallout"
|
|
"quadArrowCallout"
|
|
"bentArrow"
|
|
"uturnArrow"
|
|
"circularArrow"
|
|
"leftCircularArrow"
|
|
"leftRightCircularArrow"
|
|
"curvedRightArrow"
|
|
"curvedLeftArrow"
|
|
"curvedUpArrow"
|
|
"curvedDownArrow"
|
|
"swooshArrow"
|
|
"cube"
|
|
"can"
|
|
"lightningBolt"
|
|
"heart"
|
|
"sun"
|
|
"moon"
|
|
"smileyFace"
|
|
"irregularSeal1"
|
|
"irregularSeal2"
|
|
"foldedCorner"
|
|
"bevel"
|
|
"frame"
|
|
"halfFrame"
|
|
"corner"
|
|
"diagStripe"
|
|
"chord"
|
|
"arc"
|
|
"leftBracket"
|
|
"rightBracket"
|
|
"leftBrace"
|
|
"rightBrace"
|
|
"bracketPair"
|
|
"bracePair"
|
|
"straightConnector1"
|
|
"bentConnector2"
|
|
"bentConnector3"
|
|
"bentConnector4"
|
|
"bentConnector5"
|
|
"curvedConnector2"
|
|
"curvedConnector3"
|
|
"curvedConnector4"
|
|
"curvedConnector5"
|
|
"callout1"
|
|
"callout2"
|
|
"callout3"
|
|
"accentCallout1"
|
|
"accentCallout2"
|
|
"accentCallout3"
|
|
"borderCallout1"
|
|
"borderCallout2"
|
|
"borderCallout3"
|
|
"accentBorderCallout1"
|
|
"accentBorderCallout2"
|
|
"accentBorderCallout3"
|
|
"wedgeRectCallout"
|
|
"wedgeRoundRectCallout"
|
|
"wedgeEllipseCallout"
|
|
"cloudCallout"
|
|
"cloud"
|
|
"ribbon"
|
|
"ribbon2"
|
|
"ellipseRibbon"
|
|
"ellipseRibbon2"
|
|
"leftRightRibbon"
|
|
"verticalScroll"
|
|
"horizontalScroll"
|
|
"wave"
|
|
"doubleWave"
|
|
"plus"
|
|
"flowChartProcess"
|
|
"flowChartDecision"
|
|
"flowChartInputOutput"
|
|
"flowChartPredefinedProcess"
|
|
"flowChartInternalStorage"
|
|
"flowChartDocument"
|
|
"flowChartMultidocument"
|
|
"flowChartTerminator"
|
|
"flowChartPreparation"
|
|
"flowChartManualInput"
|
|
"flowChartManualOperation"
|
|
"flowChartConnector"
|
|
"flowChartPunchedCard"
|
|
"flowChartPunchedTape"
|
|
"flowChartSummingJunction"
|
|
"flowChartOr"
|
|
"flowChartCollate"
|
|
"flowChartSort"
|
|
"flowChartExtract"
|
|
"flowChartMerge"
|
|
"flowChartOfflineStorage"
|
|
"flowChartOnlineStorage"
|
|
"flowChartMagneticTape"
|
|
"flowChartMagneticDisk"
|
|
"flowChartMagneticDrum"
|
|
"flowChartDisplay"
|
|
"flowChartDelay"
|
|
"flowChartAlternateProcess"
|
|
"flowChartOffpageConnector"
|
|
"actionButtonBlank"
|
|
"actionButtonHome"
|
|
"actionButtonHelp"
|
|
"actionButtonInformation"
|
|
"actionButtonForwardNext"
|
|
"actionButtonBackPrevious"
|
|
"actionButtonEnd"
|
|
"actionButtonBeginning"
|
|
"actionButtonReturn"
|
|
"actionButtonDocument"
|
|
"actionButtonSound"
|
|
"actionButtonMovie"
|
|
"gear6"
|
|
"gear9"
|
|
"funnel"
|
|
"mathPlus"
|
|
"mathMinus"
|
|
"mathMultiply"
|
|
"mathDivide"
|
|
"mathEqual"
|
|
"mathNotEqual"
|
|
"cornerTabs"
|
|
"squareTabs"
|
|
"plaqueTabs"
|
|
"chartX"
|
|
"chartStar"
|
|
"chartPlus"
|
|
'''
|
|
|
|
def __init__(self,
|
|
chart,
|
|
coordinates=((0, 0), (1, 1)),
|
|
text=None,
|
|
scheme="accent1"):
|
|
self.chart = chart
|
|
self.coordinates = coordinates # in axis units
|
|
self.text = text
|
|
self.scheme = scheme
|
|
self.style = Shape.RECT
|
|
self.border_width = 0
|
|
self.border_color = Color.BLACK # "F3B3C5"
|
|
self.color = Color.WHITE
|
|
self.text_color = Color.BLACK
|
|
|
|
@property
|
|
def border_color(self):
|
|
return self._border_color
|
|
|
|
@border_color.setter
|
|
def border_color(self, color):
|
|
self._border_color = short_color(color)
|
|
|
|
@property
|
|
def color(self):
|
|
return self._color
|
|
|
|
@color.setter
|
|
def color(self, color):
|
|
self._color = short_color(color)
|
|
|
|
@property
|
|
def text_color(self):
|
|
return self._text_color
|
|
|
|
@text_color.setter
|
|
def text_color(self, color):
|
|
self._text_color = short_color(color)
|
|
|
|
@property
|
|
def border_width(self):
|
|
return self._border_width
|
|
|
|
@border_width.setter
|
|
def border_width(self, w):
|
|
self._border_width = w
|
|
|
|
@property
|
|
def coordinates(self):
|
|
"""Return coordindates in axis units"""
|
|
return self._coordinates
|
|
|
|
@coordinates.setter
|
|
def coordinates(self, coords):
|
|
""" set shape coordinates in percentages (left, top, right, bottom)
|
|
"""
|
|
# this needs refactoring to reflect changes in charts
|
|
self.axis_coordinates = coords
|
|
(x1, y1), (x2, y2) = coords # bottom left, top right
|
|
drawing_width = pixels_to_EMU(self.chart.drawing.width)
|
|
drawing_height = pixels_to_EMU(self.chart.drawing.height)
|
|
plot_width = drawing_width * self.chart.width
|
|
plot_height = drawing_height * self.chart.height
|
|
|
|
margin_left = self.chart._get_margin_left() * drawing_width
|
|
xunit = plot_width / self.chart.get_x_units()
|
|
|
|
margin_top = self.chart._get_margin_top() * drawing_height
|
|
yunit = self.chart.get_y_units()
|
|
|
|
x_start = (margin_left + (float(x1) * xunit)) / drawing_width
|
|
y_start = ((margin_top
|
|
+ plot_height
|
|
- (float(y1) * yunit))
|
|
/ drawing_height)
|
|
|
|
x_end = (margin_left + (float(x2) * xunit)) / drawing_width
|
|
y_end = ((margin_top
|
|
+ plot_height
|
|
- (float(y2) * yunit))
|
|
/ drawing_height)
|
|
|
|
# allow user to specify y's in whatever order
|
|
# excel expect y_end to be lower
|
|
if y_end < y_start:
|
|
y_end, y_start = y_start, y_end
|
|
|
|
self._coordinates = (
|
|
self._norm_pct(x_start), self._norm_pct(y_start),
|
|
self._norm_pct(x_end), self._norm_pct(y_end)
|
|
)
|
|
|
|
@staticmethod
|
|
def _norm_pct(pct):
|
|
""" force shapes to appear by truncating too large sizes """
|
|
if pct > 1:
|
|
return 1
|
|
elif pct < 0:
|
|
return 0
|
|
return pct
|
|
|
|
|
|
def bounding_box(bw, bh, w, h):
|
|
"""
|
|
Returns a tuple (new_width, new_height) which has the property
|
|
that it fits within box_width and box_height and has (close to)
|
|
the same aspect ratio as the original size
|
|
"""
|
|
new_width, new_height = w, h
|
|
if bw and new_width > bw:
|
|
new_width = bw
|
|
new_height = new_width / (float(w) / h)
|
|
if bh and new_height > bh:
|
|
new_height = bh
|
|
new_width = new_height * (float(w) / h)
|
|
return (new_width, new_height)
|
|
|
|
|
|
class Image(object):
|
|
""" Raw Image class """
|
|
|
|
@staticmethod
|
|
def _import_image(img):
|
|
try:
|
|
try:
|
|
import Image as PILImage
|
|
except ImportError:
|
|
from PIL import Image as PILImage
|
|
except ImportError:
|
|
raise ImportError('You must install PIL to fetch image objects')
|
|
|
|
if not isinstance(img, PILImage.Image):
|
|
img = PILImage.open(img)
|
|
|
|
return img
|
|
|
|
def __init__(self, img, coordinates=((0, 0), (1, 1)), size=(None, None),
|
|
nochangeaspect=True, nochangearrowheads=True):
|
|
|
|
self.image = self._import_image(img)
|
|
self.nochangeaspect = nochangeaspect
|
|
self.nochangearrowheads = nochangearrowheads
|
|
|
|
# the containing drawing
|
|
self.drawing = Drawing()
|
|
self.drawing.coordinates = coordinates
|
|
|
|
newsize = bounding_box(size[0], size[1],
|
|
self.image.size[0], self.image.size[1])
|
|
size = newsize
|
|
self.drawing.width = size[0]
|
|
self.drawing.height = size[1]
|
|
|
|
self.drawing.anchortype = None
|
|
|
|
def anchor(self, cell, anchortype="absolute"):
|
|
""" anchors the image to the given cell
|
|
optional parameter anchortype supports 'absolute' or 'oneCell'"""
|
|
self.drawing.anchortype = anchortype
|
|
if anchortype == "absolute":
|
|
self.drawing.left, self.drawing.top = cell.anchor
|
|
return ((cell.column, cell.row),
|
|
cell.parent.point_pos(self.drawing.top + self.drawing.height,
|
|
self.drawing.left + self.drawing.width))
|
|
elif anchortype == "oneCell":
|
|
self.drawing.anchorcol = column_index_from_string(cell.column) - 1
|
|
self.drawing.anchorrow = cell.row - 1
|
|
return ((self.drawing.anchorcol, self.drawing.anchorrow), None)
|
|
else:
|
|
raise ValueError("unknown anchortype %s" % anchortype)
|
|
|