[Python] Shape Drawing Add-In Function

Shared Libraries
Forum rules
For sharing working examples of macros / scripts. These can be in any script language supported by OpenOffice.org [Basic, Python, Netbean] or as source code files in Java or C# even - but requires the actual source code listing. This section is not for asking questions about writing your own macros.
Post Reply
User avatar
Charlie Young
Volunteer
Posts: 1559
Joined: Fri May 14, 2010 1:07 am

[Python] Shape Drawing Add-In Function

Post by Charlie Young »

A recent thread in the Calc forum got me started on this. As discussed there, it is possible for a cell function to insert shapes into a sheet's DrawPage. In that thread I posted a Basic function, which I have now considerably extended using Python.

The function prototype is

Code: Select all

PyDraw(dims, ShapeName, FillSpecs, ShapeText)
The arguments are

dims (required)

This is an array or CellRange specifying the ShapeType, sheet, position, size, and optional rotaion. Each row is a shape.

Column 1 is the sheet number.

Column 2 is the x coordinate in centimeters of the upper left corner of the shape's bounding rectangle. (0,0) is the upper left corner of Cell A1, of course.

Column 3 is the y coordinate of the upper left corner, this increases down the page.

Columns 4 and 5 are the width and height in cm.

Column 6 is degrees of counterclockwise rotation about the shape's center. This can change the actual position such that the shape is partially rotated off the sheet, but I didn't think this would be much of a problem.

Column 7 is the shape type: 0 is a RectangleShape, 1 is an EllipseShape, and 2 is a TextShape. A TextShape here doesn't seem to be anything more than a RectangleShape without the default boundary lines. In the case of an EllipseShape, the width and height are the major and minor axes, which, of course, becomes a circle when these are equal.

The ShapeName is used by the function to name the shapes. When they are inserted, the function names them ShapeName_0, ShapeName_1, etc. With this scheme in place, the function can delete all its previous entries when the parameters are changed prior to inserting new ones.

The FillSpecs are actually optional, but will generally be specified, and are also where most of the action is,

If the entry in the first column is a number, the FillStyle will be a solid color specified by that number, and the second column is an optional transparency percentage, 0-100.

The first column may also be a letter (case insensitive), specifying the FillStyle. The options are

n - NONE

s - SOLID, a solid color. For this, the second column becomes the color number and the third column is the transparency %.

g - GRADIENT. If this is chosen, the second column is the name as normally seen in the UI, "Gradient 1," "Rectangular red/white," etc., and the third column is the transparency.

h - HATCH. Second column name, third column transparency.

b - BITMAP. Second column name ("Metal," "Sky"...), third column transparency.

u - Also BITMAP, but the second column is a url for the bitmap instead of the name.

Then we have optional ShapeText.

The first column is the text to be entered in the shape.

The second column offers some formatting options: b - bold, u - underline, i - italic.

The third column specifies the TextVerticalAdjust and the fourth column is the TextHorizontalAdjust. These should be entered by their actual names, TOP, LEFT, BLOCK, etc., though I have made it case-insensitive.

Columns five, six and seven are the character height (points), the font color (long), and the font name.

It will be noted, of course, that much, much, more could be done with the text formatting.

The following picture show the result of entering =PYDRAW(A2:G5;"JackFlash";H2:J5;K2:Q5) in cell A9. The return value is just the ShapeName, but it will also include error information, such as invalid coordinates or fill name.

JackFlash2.jpg

One unusual thing about this function is that it's results remain even if the function is deleted from its cell. That is, the shapes remain - so I also provide a utility function DELPYDRAW, which when entered with a ShapeName, will delete all the corresponding shapes from all sheets, returning the number of shapes deleted. DELPYDRAW may then be deleted after its job is done.

Another quirk is that multiple instances of the function can have the same ShapeName. In this case, the last instance calculated "wins," deleting the shapes from all previously calculated instances.

Here is the .idl

Code: Select all

#include <com/sun/star/uno/XInterface.idl>
#include <com/sun/star/beans/XPropertySet.idl>

module com { module pydraw {

    interface XPyDraw
    {
       string PyDraw( [in] sequence< sequence< any > > dims, [in] string ShapeName, [in] any FillSpecs, [in] any ShapeText);
	   long delPyDraw( [in] string ShapeName );
	};

}; };
And the complete code, followed by the zipped .oxt file.

Code: Select all

import uno
import unohelper
import math
from com.pydraw import XPyDraw
from com.sun.star.awt import Size, Point
from com.sun.star.drawing import HomogenMatrix3, HomogenMatrixLine3

#Set up enum classes.
class FillStyle():
    from com.sun.star.drawing.FillStyle import (NONE, SOLID, GRADIENT, HATCH, BITMAP)

class BitmapMode():
    from com.sun.star.drawing.BitmapMode import (REPEAT, STRETCH, NO_REPEAT)
	
class FontWeight():
    from com.sun.star.awt.FontWeight import (NORMAL, BOLD)

class FontUnderline():
    from com.sun.star.awt.FontUnderline import (SINGLE, NONE)

class FontSlant():
    from com.sun.star.awt.FontSlant import (NONE, ITALIC)

class TextHorizontalAdjust():
    from com.sun.star.drawing.TextHorizontalAdjust import (LEFT, CENTER, RIGHT, BLOCK)
	
class TextVerticalAdjust():
    from com.sun.star.drawing.TextVerticalAdjust import (TOP, CENTER, BOTTOM, BLOCK)

# Draw Shapes add-in function.

class PyDrawImpl( unohelper.Base, XPyDraw ):
    def __init__( self, ctx ):
        self.ctx = ctx

    def PyDraw(self, dims, ShapeName, FillSpecs, ShapeText):
        desktop = self.ctx.getServiceManager().createInstanceWithContext("com.sun.star.frame.Desktop", self.ctx)
        oDoc = desktop.getCurrentComponent()
        oSheets = oDoc.getSheets()
        
        #Gets style for default text properties.
        defaultStyle = oDoc.getStyleFamilies().getByName("CellStyles").getByName("Default")
        
        ShapeServices = ["com.sun.star.drawing.RectangleShape","com.sun.star.drawing.EllipseShape","com.sun.star.drawing.TextShape"]
        
        ShapesIn = getShapes(dims)

        #Erase any existing shapes named ShapeName_number
        self.delPyDraw(ShapeName)
        ReturnStr = ShapeName
        hasError = False
        pyShapes= []

        #Check shapes for valid coordinates and type.
        for i in range(len(ShapesIn)):
            ShapeError = False
            pyShape = ShapesIn[i]
            
            sheet, x, y, w, h, Rotate, ShapeType = map(float,pyShape)
            sheet = int(sheet)
            if sheet < 0 or sheet >= oSheets.getCount():
                if not ShapeError:
                    ShapeError = True
                    ReturnStr = ReturnStr + ": " if not hasError else ReturnStr + ", "
                else:
                    ReturnStr = ReturnStr + ", "
                ReturnStr = ReturnStr + "Shape " + str(i) + " Sheet out of range"
            else:
                oSheet = oSheets.getByIndex(sheet)
                x, y, w, h, ShapeType = long(1000*x), long(1000*y), long(1000*w), long(1000*h), int(ShapeType)
                SheetPos = oSheet.Position
                SheetSize = oSheet.Size
                if (x < SheetPos.X or x + w > SheetPos.X + SheetSize.Width) or (y < SheetPos.Y or y + h > SheetPos.Y + SheetSize.Height) or (w <= 0 or h <= 0):
                    if not ShapeError:
                        ShapeError = True
                        ReturnStr = ReturnStr + ": " if not hasError else ReturnStr + ", "
                    else:
                        ReturnStr = ReturnStr + ", "
                    ReturnStr = ReturnStr + "Shape " + str(i) + " dimensions out of range"
                elif ShapeType < 0 or ShapeType > 2:
                    if not ShapeError:
                        ShapeError = True
                        ReturnStr = ReturnStr + ": " if not hasError else ReturnStr + ", "
                    else:
                        ReturnStr = ReturnStr + ", "
                    ReturnStr = ReturnStr + "Shape " + str(i) + " Invalid shape type"
            #add shape if it's OK.    
            if not ShapeError:
                 pyShapes.append(pyShape)
            else:
                 hasError = True
           
        #pyShape = [sheet,x,y,width,height,theta,type]
        fillList = [FillStyle.NONE,FillStyle.SOLID,FillStyle.GRADIENT,FillStyle.HATCH,FillStyle.BITMAP,FillStyle.BITMAP]
        
        #Set defaults for Gradient, Hatch, and Bitmap fill.
        fillDefaults = ["Gradient 1","Black 0 degrees","Blank"]
        #Insert the shapes
        for i in range(len(pyShapes)):
            pyShape = pyShapes[i]
            sheet, x, y, w, h, Rotate, ShapeType = map(float,pyShape)
            x, y, w, h, ShapeType = long(1000*x), long(1000*y), long(1000*w), long(1000*h), int(ShapeType)
            filltrans = 0
            fillurl = False
            if type(FillSpecs) == tuple:
                FillSpec = FillSpecs[i] if len(FillSpecs) > i else [float(-1)]
                if type(FillSpec[0]) == float:
                    fillstyle = FillStyle.SOLID
                    FillColor = int(FillSpec[0])
                    # Read transparency value from second column.
                    filltrans = 0 if len(FillSpec) < 2 or type(FillSpec[1]) != float else int(FillSpec[1]) if 0 < FillSpec[1] <= 100 else 0 
                elif type(FillSpec[0]) == unicode:
                    if len(FillSpec[0]) > 0:
                        #Get fill type from letter code.
                        fillIndex = "nsghbu".find(FillSpec[0][0].lower())
                        if fillIndex != -1:
                            fillstyle = fillList[fillIndex]
                            fillurl = False if fillIndex < 5 else True
                        else:
                            ReturnStr = ReturnStr + ": " if not hasError else ReturnStr + ", "
                            ReturnStr = ReturnStr + "Shape " + str(i) + " Invalid fill, set to SOLID"
                            hasError = True
                    else:
                        fillstyle = FillStyle.SOLID #default to solid color.
                    if fillstyle in [FillStyle.GRADIENT,FillStyle.HATCH,FillStyle.BITMAP]:
                        #Get the fill name
                        if len(FillSpec) > 1 and type(FillSpec[1]) == unicode:
                            fillname = FillSpec[1] if len(FillSpec[1]) > 0 else fillDefaults[fillIndex - 2]
                        else:
                            fillname = fillDefaults[fillIndex - 2]
                        #Read transparency value from third column
                        filltrans = 0 if len(FillSpec) < 3 or type(FillSpec[2]) != float else int(FillSpec[2]) if 0 < FillSpec[2] <= 100 else 0
            elif type(FillSpecs) == float:
                #Default to solid color fill for all shapes with color FillSpecs
                fillstyle = FillStyle.SOLID
                FillColor = int(FillSpecs)
            else:
                fillstyle = FillStyle.SOLID
                FillColor = -1
   
            hasText = False	
            if type(ShapeText) == tuple:
                TextSpec = ShapeText[i] if len(ShapeText) > i else [""]
                ShapeString = ""
                if type(TextSpec[0]) == unicode and len(TextSpec[0]) > 0:
                    hasText = True
                    ShapeString = TextSpec[0]
                
                if hasText:
                    if len(TextSpec) > 1 and type(TextSpec[1]) == unicode and len(TextSpec[1]) > 0:
                        TextBold = FontWeight.BOLD if "b" in TextSpec[1].lower() else FontWeight.NORMAL
                        TextUnder = FontUnderline.SINGLE if "u" in TextSpec[1].lower() else FontUnderline.NONE
                        TextItalic = FontSlant.ITALIC if "i" in TextSpec[1].lower() else FontSlant.NONE
                    else:
                        TextBold = FontWeight.NORMAL
                        TextUnder = FontUnderline.NONE
                        TextItalic = FontSlant.NONE
                    
                    if len(TextSpec) > 2 and type(TextSpec[2]) == unicode:
                        if TextSpec[2].upper() == "TOP":
						   TextVertical = TextVerticalAdjust.TOP
                        elif TextSpec[2].upper() == "BOTTOM":
                           TextVertical = TextVerticalAdjust.BOTTOM
                        elif TextSpec[2].upper() == "BLOCK":
                           TextVertical = TextVerticalAdjust.BLOCK
                        else:
                           TextVertical = TextVerticalAdjust.CENTER
                    else:
                        TextVertical = TextVerticalAdjust.CENTER
                                            
                    if len(TextSpec) > 3 and type(TextSpec[3]) == unicode:
                        if TextSpec[3].upper() == "LEFT":
						   TextHorizontal = TextHorizontalAdjust.LEFT
                        elif TextSpec[3].upper() == "RIGHT":
                           TextHorizontal = TextHorizontalAdjust.RIGHT
                        elif TextSpec[3].upper() == "BLOCK":
                           TextHorizontal = TextHorizontalAdjust.BLOCK
                        else:
                           TextHorizontal = TextHorizontalAdjust.CENTER
                    else:
                        TextHorizontal = TextHorizontalAdjust.CENTER
                    if len(TextSpec) > 4 and type(TextSpec[4]) == float:
                        TextHeight = TextSpec[4] if TextSpec[4] > 0 else defaultStyle.CharHeight
                    else:
                        TextHeight = defaultStyle.CharHeight
                    if len(TextSpec) > 5 and type(TextSpec[5]) == float:
                        TextColor = long(TextSpec[5]) if TextSpec[5] > 0 else defaultStyle.CharColor
                    else:
                        TextColor = defaultStyle.CharColor
                    if len(TextSpec) > 6 and type(TextSpec[6]) == unicode:
                        TextFontName = TextSpec[6] if len(TextSpec[6]) > 0 else defaultStyle.CharFontName
                    else:
                        TextFontName = defaultStyle.CharFontName  
            
            oShape = oDoc.createInstance(ShapeServices[ShapeType])
            oShape.Name = ShapeName + "_" + str(i)
            sheet = pyShape[0]
            oShapes = oSheets.getByIndex(sheet).getDrawPage()
            oShapes.add(oShape)
            oShape.FillStyle = fillstyle
            try:
                if fillstyle == FillStyle.GRADIENT:
                    oShape.FillGradientName = fillname
                elif fillstyle == FillStyle.HATCH:
                    oShape.FillHatchName = fillname
                elif fillstyle == FillStyle.BITMAP:
                    if not fillurl:
                        oShape.FillBitmapName = fillname
                    else:
                        oShape.FillBitmapURL = fillname
                        oShape.FillBitmapMode = BitmapMode.STRETCH
                else:
                    oShape.FillColor = FillColor
            except:
                ReturnStr = ReturnStr + ": " if not hasError else ReturnStr + ", "
                ReturnStr = ReturnStr + "Shape " + str(i) + " Invalid fill name, set to default."
                hasError = True
                if fillstyle == FillStyle.GRADIENT:
                    oShape.FillGradientName = "Gradient 1"
                elif fillstyle == FillStyle.HATCH:
                    oShape.FillHatchName = "Black 0 degrees"
                elif fillstyle == FillStyle.BITMAP:
                    oShape.FillBitmapName = "Blank"
                else:
                    oShape.FillColor = FillColor
            
            if hasText:
                try:
                    oShape.String = ShapeString
                    oShape.CharWeight = TextBold
                    oShape.CharUnderline = TextUnder 
                    oShape.CharPosture = TextItalic
                    oShape.CharHeight = TextHeight
                    oShape.CharColor = TextColor
                    oShape.CharFontName = TextFontName
                    oShape.TextHorizontalAdjust = TextHorizontal
                    oShape.TextVerticalAdjust = TextVertical
                except:
                    ReturnStr = ReturnStr + ": " if not hasError else ReturnStr + ", "
                    ReturnStr = ReturnStr + "Shape " + str(i) + " Invalid text format, set to default"
                    hasError = True
                    oShape.String = ShapeString
                    oShape.CharWeight = defaultStyle.CharWeight
                    oShape.CharUnderline = defaultStyle.CharUnderline
                    oShape.CharPosture = defaultStyle.CharPosture
                    oShape.CharHeight = defaultStyle.CharHeight
                    oShape.CharColor = defaultStyle.CharColor
                    oShape.CharFontName = defaultStyle.CharFontName
                    oShape.TextHorizontalAdjust = TextHorizontalAdjust.CENTER
                    oShape.TextVerticalAdjust = TextVerticalAdjust.CENTER
              				
            oShape.FillTransparence = filltrans
            s = Size(w, h)
            p = Point(x, y)
        
            theta = math.radians(Rotate)
            oShape.setSize(s)
            oShape.setPosition(p)
            #Apply rotation
            if theta != 0:
                tMatrix = HomogenMatrix3()
                l1 = HomogenMatrixLine3()
                l2 = HomogenMatrixLine3()
                X = oShape.getPropertyValue("Transformation")
                xMatrix = [[0,0],[0,0]]
                xMatrix[0][0] = X.Line1.Column1
                xMatrix[0][1] = X.Line1.Column2
                xMatrix[1][0] = X.Line2.Column1
                xMatrix[1][1] = X.Line2.Column2
                
                #Rotate the transformation matrix.
                stheta = math.sin(theta)
                ctheta = math.cos(theta)
                l1.Column1 = xMatrix[0][0] * ctheta + xMatrix[1][0] * stheta
                l2.Column1 = -xMatrix[0][0] * stheta + xMatrix[1][0] * ctheta
                l1.Column2 = xMatrix[0][1] * ctheta + xMatrix[1][1] * stheta
                l2.Column2 = -xMatrix[0][1] * stheta + xMatrix[1][1] * ctheta
                
                #Adjust position to rotate about center.
                l1.Column3 = X.Line1.Column3 + long((w - w * ctheta - h * stheta)/2) 
                l2.Column3 = X.Line2.Column3 + long((h - h * ctheta + w * stheta)/2)
                tMatrix.Line1 = l1
                tMatrix.Line2 = l2
                tMatrix.Line3 = X.Line3
                oShape.setPropertyValue("Transformation",tMatrix)
        
        return ReturnStr
		
    #Deletes all shapes named ShapeName_number
    def delPyDraw(self, ShapeName):	
        desktop = self.ctx.getServiceManager().createInstanceWithContext("com.sun.star.frame.Desktop", self.ctx)
        oDoc = desktop.getCurrentComponent()
        ShapeServices = ["com.sun.star.drawing.RectangleShape","com.sun.star.drawing.EllipseShape","com.sun.star.drawing.TextShape"]
        oSheets =  oDoc.getSheets()
        delCount = 0
        for i in range(oSheets.getCount()):
            oSheet = oSheets.getByIndex(i)
            oShapes = oSheet.getDrawPage()
            c = oShapes.getCount()
            while c > 0:
                oShape = oShapes.getByIndex(c - 1) 
                if oShape.getShapeType() in ShapeServices and oShape.getName().startswith(ShapeName + "_"):
                    oShapes.remove(oShape)
                    delCount += 1
                c -= 1

        return delCount


def getShapes(dims):
    dimlen = len(dims)
    dimlen0 = len(dims[0])
    pyShapes = [tuple([int(dims[i][0]) if type(dims[i][0]) == float else 0] + [dims[i][j] if j < dimlen0 else 0 for j in range(1,7)]) for i in range(dimlen)]
    return pyShapes
	
def createInstance( ctx ):
    return PyDrawImpl( ctx )

g_ImplementationHelper = unohelper.ImplementationHelper()
g_ImplementationHelper.addImplementation(
   createInstance,"com.pydraw.python.PyDrawImpl",
      ("com.sun.star.sheet.AddIn",),)
Attachments
PyDraw.zip
PYDRAW, version 1.00
(5.61 KiB) Downloaded 379 times
Apache OpenOffice 4.1.1
Windows XP
Post Reply