'From Moshi of 3 March 2007 [latest update: #2378] on 8 April 2011 at 1:28:31 pm'! "Change Set: ODFReader11-tak Date: 7 April 2011 Author: Takashi Yamamiya Support draw:custom-shape. Note: Some shapes are not drawn well because there are still unimplemented path commands and the gradient fill has bugs."! Object subclass: #ODFFormulaInterpreter instanceVariableNames: 'modifiers functions viewBox stretchpoint style' classVariableNames: '' poolDictionaries: '' category: 'ODFReader'! !ODFFormulaInterpreter commentStamp: 'tak 4/6/2011 15:50' prior: 0! It evaluates an expression built by ODFFormulaParser Structure: modifiers Array -- a list of numbers in draw:modifires functions IdentityDictionary -- a list of function expressions viewBox Rectangle -- from draw:viewBox style ODFStyle -- from draw:style Bug: logwidth and logheight are not defined correctly.! OMeta2 subclass: #ODFFormulaParser instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'ODFReader'! !ODFFormulaParser commentStamp: 'tak 4/7/2011 10:17' prior: 0! A parser for draw:formula attribute used in draw:custom-shape. See ODFReaderTest >> testFormulaParser http://docs.oasis-open.org/office/v1.1/OS/OpenDocument-v1.1-html/OpenDocument-v1.1.html#outline:9.5.5.Enhanced_Geometry___Equation Note: This BNF has a bug in atan2, function_reference, modifier_reference, and unary_expression, so I fixed them. ODFFormulaParser parse: '3+4'! !LSVGPathReader methodsFor: 'public' stamp: 'tak 4/7/2011 15:09'! readPath: aString stream := aString readStream. [stream skipSeparators; atEnd] whileFalse: [ stream skipSeparatorsAndPeekNext isLetter ifTrue: [command := stream next]. command asUppercase caseOf: { [$M] -> [self moveTo: self nextPoint]. [$L] -> [self line: self currentPoint to: self nextPoint]. [$H] -> [self line: self currentPoint to: self nextPointH]. [$V] -> [self line: self currentPoint to: self nextPointV]. [$C] -> [self bezier: self currentPoint via: self nextPoint and: self nextPoint to: self nextPoint]. [$S] -> [self bezier: self currentPoint via: self smoothPoint and: self nextPoint to: self nextPoint]. [$Q] -> [self bezier: self currentPoint via: self nextPoint to: self nextPoint]. [$T] -> [self bezier: self currentPoint via: self smoothPoint to: self nextPoint]. [$A] -> [self arc: self currentPoint radius: self readPoint rotate: self readNumber large: self readBool sweep: self readBool to: self nextPoint]. [$Z] -> [self closePath]. [$N] -> [Transcript cr; show: 'path N is not implemented'.]. [$F] -> [Transcript cr; show: 'path F is not implemented'.]. [$U] -> [Transcript cr; show: 'path U is not implemented'. self nextPoint. self nextPoint. self nextPoint]. [$B] -> [Transcript cr; show: 'path B is not implemented'. self nextPoint. self nextPoint. self nextPoint. self nextPoint]. [$W] -> [Transcript cr; show: 'path W is not implemented'. self nextPoint. self nextPoint. self nextPoint. self nextPoint]. [$X] -> [Transcript cr; show: 'path X is not implemented'. self nextPoint]. [$Y] -> [Transcript cr; show: 'path Y is not implemented'. self nextPoint]. }]. self newPath. "flush" ^paths! ! !ODFDocument methodsFor: 'render' stamp: 'tak 4/7/2011 15:01'! renderDrawCustomShape: xml "Return a draw-path made from xml" | box shape points matrix drawStyle pathString | box := LBox new. drawStyle := self styleNamed: xml @ #'draw:style-name'. pathString := ODFFormulaInterpreter renderDrawEnhancedGeometry: xml / #'draw:enhanced-geometry' style: drawStyle. points := LSVGPathReader new readPath: pathString. matrix := self renderViewBox: xml. points isEmpty ifTrue: [^ self renderDrawEllipse: xml. ] ifFalse: [ shape := LGenericShape new elements: {{points first collect: [:p | matrix localPointToGlobal: p]} asGeziraPath}]. box shape: shape. self renderDrawShapeWithTextAndStyles: xml on: box. self renderDrawTransform: xml offset: shape offset negated on: box. ^ box! ! !ODFDocument methodsFor: 'render' stamp: 'tak 4/7/2011 14:37'! renderShape: xml ^ xml localName caseOf: { [#draw:circle] -> [self renderDrawCircle: xml]. [#'draw:custom-shape'] -> [self renderDrawCustomShape: xml]. [#draw:ellipse] -> [self renderDrawEllipse: xml]. [#draw:frame] -> [self renderDrawFrame: xml]. [#draw:g] -> [self renderDrawG: xml]. [#draw:line] -> [self renderDrawLine: xml]. [#draw:path] -> [self renderDrawPath: xml]. [#draw:polygon] -> [self renderDrawPolygon: xml]. [#draw:polyline] -> [self renderDrawPolyline: xml]. [#draw:rect] -> [self renderDrawRect: xml]. [#anim:par] -> ["see sender of #renderAnimPar:on:". nil]. } otherwise: [Transcript cr; show: 'Unsupported shape: ', xml localName. nil]! ! !ODFDocument methodsFor: 'render' stamp: 'tak 4/7/2011 09:53'! renderViewBox: xml "Return a matrix which converts viewBox coordinates to real one. viewBox only affects points, but not other properties like border width." | x y viewBox scale width height | x := self class pixelsFromString: xml @ #svg:x. y := self class pixelsFromString: xml @ #svg:y. width := self class pixelsFromString: xml @ #svg:width. height := self class pixelsFromString: xml @ #svg:height. viewBox := self class readViewBox: xml. scale := width / viewBox width @ (viewBox height isZero ifTrue: [1] ifFalse: [height / viewBox height]). ^ (MatrixTransform2x3 withScale: scale) offset: viewBox left @ viewBox top * scale + (x @ y)! ! !ODFDocument class methodsFor: 'utilities' stamp: 'tak 4/7/2011 15:04'! readViewBox: xml "Return a Rectangle from the xml. todo: cleanup a case of custom-shape (draw:enhanced-geometry)" | viewBox | viewBox := xml attributeAt: #svg:viewBox ifAbsent: [xml / #'draw:enhanced-geometry' @ #svg:viewBox]. ^ (viewBox findTokens: ' ') in: [:q | q first asNumber @ q second asNumber corner: q third asNumber @ q fourth asNumber]! ! !ODFFormulaInterpreter methodsFor: 'eval' stamp: 'tak 4/7/2011 13:15'! evalPath: pathString "Because custom-shape is generated by LSVGPathReader, the output of the interpreter is a string instead of a list of coordinates." | elements aStream | elements := ODFFormulaParser parsePath: pathString. aStream := WriteStream on: (String new: 100). elements do: [:each | each isCharacter ifTrue: [aStream nextPut: each] ifFalse: [aStream print: (self eval: each); space]]. ^ aStream contents! ! !ODFFormulaInterpreter methodsFor: 'eval' stamp: 'tak 4/7/2011 10:56'! eval: expr | args | expr isNumber ifTrue: [^ expr]. expr isSymbol ifTrue: [^ self perform: expr]. expr first = #f ifTrue: [^ self eval: (functions at: expr second)]. args := expr allButFirst collect: [:each | self eval: each]. ^ expr first caseOf: { [#negated] -> [args first negated]. [#+] -> [args first + args second]. [#-] -> [args first - args second]. [#*] -> [args first * args second]. [#/] -> [args first / args second]. [#abs] -> [args first abs]. [#sqrt] -> [args first sqrt]. [#sin] -> [args first sin]. [#cos] -> [args first cos]. [#tan] -> [args first tan]. [#atan] -> [args first arcTan]. [#atan2] -> [args first arcTan: args second]. [#min] -> [args first min: args second]. [#max] -> [args first max: args second]. [#if] -> [args first = 1 ifTrue: [args second] ifFalse: [args third]]. [#m] -> [modifiers at: args first + 1]}! ! !ODFFormulaInterpreter methodsFor: 'expressions' stamp: 'tak 4/6/2011 15:47'! bottom ^ viewBox bottom! ! !ODFFormulaInterpreter methodsFor: 'expressions' stamp: 'tak 4/6/2011 15:59'! hasfill style at: #draw:fill ifPresent: [:v | ^ v = 'none' ifTrue: [0] ifFalse: [1]]. ^ 0! ! !ODFFormulaInterpreter methodsFor: 'expressions' stamp: 'tak 4/6/2011 16:00'! hasstroke style at: #draw:stroke ifPresent: [:v | ^ v = 'none' ifTrue: [0] ifFalse: [1]]. ^ 0! ! !ODFFormulaInterpreter methodsFor: 'expressions' stamp: 'tak 4/6/2011 15:49'! height ^ viewBox height! ! !ODFFormulaInterpreter methodsFor: 'expressions' stamp: 'tak 4/6/2011 15:46'! left ^ viewBox left! ! !ODFFormulaInterpreter methodsFor: 'expressions' stamp: 'tak 4/6/2011 15:50'! logheight ^ viewBox height * 100! ! !ODFFormulaInterpreter methodsFor: 'expressions' stamp: 'tak 4/6/2011 15:50'! logwidth ^ viewBox width * 100! ! !ODFFormulaInterpreter methodsFor: 'expressions' stamp: 'tak 4/6/2011 15:46'! pi ^ Float pi! ! !ODFFormulaInterpreter methodsFor: 'expressions' stamp: 'tak 4/6/2011 15:47'! right ^ viewBox right! ! !ODFFormulaInterpreter methodsFor: 'expressions' stamp: 'tak 4/6/2011 15:46'! top ^ viewBox top! ! !ODFFormulaInterpreter methodsFor: 'expressions' stamp: 'tak 4/6/2011 15:48'! width ^ viewBox width! ! !ODFFormulaInterpreter methodsFor: 'expressions' stamp: 'tak 4/6/2011 15:53'! xstretch ^ stretchpoint x! ! !ODFFormulaInterpreter methodsFor: 'expressions' stamp: 'tak 4/6/2011 15:53'! ystretch ^ stretchpoint y! ! !ODFFormulaInterpreter methodsFor: 'private' stamp: 'tak 4/6/2011 15:51'! modifiers: anArray functions: anIdentityDictionary viewBox: aRectangle stretchpoint: aPoint style: anODFStyle modifiers := anArray. functions := anIdentityDictionary. viewBox := aRectangle. stretchpoint := aPoint. style := anODFStyle.! ! !ODFFormulaInterpreter class methodsFor: 'instance creation' stamp: 'tak 4/6/2011 15:52'! modifiers: anArray functions: anIdentityDictionary viewBox: aRectangle stretchpoint: aPoint style: anODFStyle ^ self new modifiers: anArray functions: anIdentityDictionary viewBox: aRectangle stretchpoint: aPoint style: anODFStyle! ! !ODFFormulaInterpreter class methodsFor: 'instance creation' stamp: 'tak 4/7/2011 14:19'! newFrom: xml style: style "Return ODFFormulaInterpreter with all necessary properties from the xml and style" | modifiers functions viewBox xstretch ystretch | modifiers := (xml @ #draw:modifiers findTokens: ' ') collect: [:each | each asNumber]. functions := self renderDrawEquations: xml. viewBox := ODFDocument readViewBox: xml. xstretch := xml attributeAt: #'draw:path-stretchpoint-x' ifAbsent: [0]. ystretch := xml attributeAt: #'draw:path-stretchpoint-y' ifAbsent: [0]. ^ self modifiers: modifiers functions: functions viewBox: viewBox stretchpoint: xstretch @ ystretch style: style! ! !ODFFormulaInterpreter class methodsFor: 'instance creation' stamp: 'tak 4/7/2011 14:24'! renderDrawEnhancedGeometry: xml style: style "Return ODFFormulaInterpreter with all necessary properties from the xml and style" | interpreter | interpreter := self newFrom: xml style: style. ^ interpreter evalPath: xml @ #'draw:enhanced-path'. ! ! !ODFFormulaInterpreter class methodsFor: 'instance creation' stamp: 'tak 4/7/2011 10:37'! renderDrawEquations: xml | fname formula | ^ (xml elements select: [:each | each localName = #draw:equation] thenCollect: [:each | fname := each @ #draw:name. formula := each @ #draw:formula. fname asSymbol -> (ODFFormulaParser parse: formula)]) as: IdentityDictionary! ! !ODFFormulaParser methodsFor: 'public' stamp: 'tak 4/7/2011 14:15'! path = (functionReference | modifierReference | anything)*! ! !ODFFormulaParser methodsFor: 'public' stamp: 'tak 4/7/2011 10:16'! start = additiveExpression! ! !ODFFormulaParser methodsFor: 'terminals' stamp: 'tak 4/7/2011 14:11'! binaryFunction = (``min'' | ``max'' | ``atan2''):s spaces -> [ s asSymbol ]! ! !ODFFormulaParser methodsFor: 'terminals' stamp: 'tak 4/7/2011 14:11'! close = $) spaces ! ! !ODFFormulaParser methodsFor: 'terminals' stamp: 'tak 4/7/2011 14:11'! comma = $, spaces ! ! !ODFFormulaParser methodsFor: 'terminals' stamp: 'tak 4/7/2011 14:11'! functionReference = $? <(range($a. $z) | range($A. $Z) | range($0. $9))+>:s spaces -> [{#f. s asSymbol}]! ! !ODFFormulaParser methodsFor: 'terminals' stamp: 'tak 4/7/2011 14:12'! identifier = (``pi'' | ``left'' | ``top'' | ``right'' | ``bottom'' | ``xstretch'' | ``ystretch'' | ``hasstroke'' | ``hasfill'' | ``width'' | ``height'' | ``logwidth'' | ``logheight''):s spaces -> [ s asSymbol ]! ! !ODFFormulaParser methodsFor: 'terminals' stamp: 'tak 4/7/2011 14:12'! modifierReference = $$ number:n spaces -> [{#m. n}] ! ! !ODFFormulaParser methodsFor: 'terminals' stamp: 'tak 4/7/2011 14:12'! open = $( spaces ! ! !ODFFormulaParser methodsFor: 'terminals' stamp: 'tak 4/7/2011 14:12'! ternaryFunction = ``if'':s spaces -> [ s asSymbol ] ! ! !ODFFormulaParser methodsFor: 'terminals' stamp: 'tak 4/7/2011 14:12'! unaryFunction = (``abs'' | ``sqrt'' | ``sin'' | ``cos'' | ``tan'' | ``atan''):s spaces -> [ s asSymbol ]! ! !ODFFormulaParser methodsFor: 'non terminals' stamp: 'tak 4/8/2011 13:26'! additiveExpression = additiveExpression:x ``+'' spaces multiplicativeExpression:y -> [{#'+'. x. y}] | additiveExpression:x ``-'' spaces multiplicativeExpression:y -> [{#'-'. x. y}] | multiplicativeExpression! ! !ODFFormulaParser methodsFor: 'non terminals' stamp: 'tak 4/7/2011 10:25'! basicExpression = number | identifier | functionReference | modifierReference | unaryFunction:f open additiveExpression:x close -> [{f. x}] | binaryFunction:f open additiveExpression:x comma additiveExpression:y close -> [{f. x. y}] | ternaryFunction:f open additiveExpression:x comma additiveExpression:y comma additiveExpression:z close -> [{f. x. y. z}] | open additiveExpression:x close -> [x] ! ! !ODFFormulaParser methodsFor: 'non terminals' stamp: 'tak 4/8/2011 13:26'! multiplicativeExpression = multiplicativeExpression:x ``*'' spaces unaryExpression:y -> [{#'*'. x. y}] | multiplicativeExpression:x ``/'' spaces unaryExpression:y -> [{#'/'. x. y}] | unaryExpression ! ! !ODFFormulaParser methodsFor: 'non terminals' stamp: 'tak 4/7/2011 10:19'! number = :n spaces -> [n asNumber]! ! !ODFFormulaParser methodsFor: 'non terminals' stamp: 'tak 4/8/2011 13:26'! unaryExpression = $- spaces basicExpression:x -> [{#negated. x}] | basicExpression! ! !ODFFormulaParser class methodsFor: 'public' stamp: 'tak 4/7/2011 13:08'! parsePath: aString ^ self matchAll: aString with: #path! ! !ODFFormulaParser class methodsFor: 'public' stamp: 'tak 4/7/2011 13:08'! parse: aString ^ self matchAll: aString with: #start! ! !ODFReaderTest methodsFor: 'running' stamp: 'tak 4/6/2011 10:16'! setUp Processor activeProcess lWindow ifNil: [Processor activeProcess lWindow: LWindow new].! ! !ODFReaderTest methodsFor: 'testing' stamp: 'tak 4/7/2011 14:19'! testFormulaEnhancedGeometry "self debug: #testFormulaEnhancedGeometry" | xml interpreter | xml := ' ' asXml elements first. interpreter := ODFFormulaInterpreter newFrom: xml style: nil. self assert: interpreter width = 1000. self assert: (interpreter eval: #(f f0)) = 500. self assert: (interpreter eval: #(f f1)) = 255. self assert: (interpreter eval: #(f f2)) = (1000 / 3). self assert: (interpreter eval: #(m 0)) = 0. self assert: (interpreter eval: #(m 1)) = 10. self assert: (interpreter evalPath: 'M 0 1000 L $0 $1 1000 1000 Z N M 0 1000 L ?f0 ?f1 F N') = 'M 0 1000 L 0 10 1000 1000 Z N M 0 1000 L 500 255 F N'.! ! !ODFReaderTest methodsFor: 'testing' stamp: 'tak 4/7/2011 10:39'! testFormulaEquations "self debug: #testFormulaEquations" | xml formulas | xml := ' ' asXml elements first. formulas := ODFFormulaInterpreter renderDrawEquations: xml. self assert: (formulas at: #f0) = #(/ (+ (m 0) right) 2). self assert: (formulas at: #f1) = #(/ (+ (m 1) bottom) 2). self assert: (formulas at: #f2) = #(/ (* 2 (- bottom top)) 3). ! ! !ODFReaderTest methodsFor: 'testing' stamp: 'tak 4/7/2011 10:55'! testFormulaInterpreter "self debug: #testFormulaInterpreter" | interpreter modifiers functions viewBox style | viewBox := 100 @ 200 corner: 200 @ 400. style := { #draw:stroke -> 'solid' } as: ODFStyle. modifiers := #(10 20 30). functions := { #func -> #(+ 3 4) } as: ODFStyle. interpreter := ODFFormulaInterpreter modifiers: modifiers functions: functions viewBox: viewBox stretchpoint: 50 @ 60 style: style. self assert: (interpreter eval: 7) = 7. self assert: (interpreter eval: #(negated 7)) = -7. self assert: (interpreter eval: #(+ 3 4)) = 7. self assert: (interpreter eval: #pi) = Float pi. self assert: (interpreter eval: #left) = 100. self assert: (interpreter eval: #top) = 200. self assert: (interpreter eval: #right) = 200. self assert: (interpreter eval: #bottom) = 400. self assert: (interpreter eval: #xstretch) = 50. self assert: (interpreter eval: #ystretch) = 60. self assert: (interpreter eval: #hasstroke) = 1. self assert: (interpreter eval: #hasfill) = 0. self assert: (interpreter eval: #width) = 100. self assert: (interpreter eval: #height) = 200. self assert: (interpreter eval: #logwidth) = 10000. self assert: (interpreter eval: #logheight) = 20000. self assert: (interpreter eval: #(abs (negated 7))) = 7. self assert: (interpreter eval: #(sqrt 9)) = 3. self assert: (interpreter eval: #(sin (/ pi 2))) = 1. self assert: (interpreter eval: #(cos 0)) = 1. self assert: (interpreter eval: #(tan (/ pi 4))) - 1 < 0.001. self assert: (interpreter eval: #(atan (tan 1))) = 1. self assert: (interpreter eval: #(atan2 (sqrt 3) 1)) - (Float pi / 3) < 0.001. self assert: (interpreter eval: #(min 1 2)) = 1. self assert: (interpreter eval: #(max 1 2)) = 2. self assert: (interpreter eval: #(if 1 2 3)) = 2. self assert: (interpreter eval: #(if 0 2 3)) = 3. self assert: (interpreter eval: #(m 2)) = 30. self assert: (interpreter eval: #(f func)) = 7. ! ! !ODFReaderTest methodsFor: 'testing' stamp: 'tak 4/7/2011 10:26'! testFormulaParser "self debug: #testFormulaParser" self assert: (ODFFormulaParser matchAll: '234' with: #number) == 234. self assert: (ODFFormulaParser matchAll: 'logheight' with: #identifier) == #logheight. self assert: (ODFFormulaParser matchAll: 'atan' with: #unaryFunction) == #atan. self assert: (ODFFormulaParser matchAll: 'max' with: #binaryFunction) == #max. self assert: (ODFFormulaParser matchAll: 'atan2' with: #binaryFunction) == #atan2. self assert: (ODFFormulaParser matchAll: 'if' with: #ternaryFunction) == #if. self assert: (ODFFormulaParser matchAll: '?aA0 ' with: #functionReference) = #(f aA0). self assert: (ODFFormulaParser matchAll: '$999 ' with: #modifierReference) = #(m 999). self assert: (ODFFormulaParser matchAll: '234' with: #basicExpression) = 234. self assert: (ODFFormulaParser matchAll: '?aA0 ' with: #basicExpression) = #(f aA0). self assert: (ODFFormulaParser matchAll: '$999 ' with: #basicExpression) = #(m 999). self assert: (ODFFormulaParser matchAll: 'logheight' with: #basicExpression) = #logheight. self assert: (ODFFormulaParser matchAll: 'atan(1)' with: #basicExpression) = #(atan 1). self assert: (ODFFormulaParser matchAll: 'atan2(1,2)' with: #basicExpression) = #(atan2 1 2). self assert: (ODFFormulaParser matchAll: 'min(1,2)' with: #basicExpression) = #(min 1 2). self assert: (ODFFormulaParser matchAll: 'if(1,2,3)' with: #basicExpression) = #(if 1 2 3). self assert: (ODFFormulaParser matchAll: '(7)' with: #basicExpression) = 7. self assert: (ODFFormulaParser matchAll: '(3+4)' with: #basicExpression) = #(+ 3 4). self assert: (ODFFormulaParser matchAll: '(3+4+5)' with: #basicExpression) = #(+ (+ 3 4) 5). self assert: (ODFFormulaParser matchAll: '(3*4*5)' with: #basicExpression) = #(* (* 3 4) 5). self assert: (ODFFormulaParser matchAll: '(3+4*5)' with: #basicExpression) = #(+3 (* 4 5)). self assert: (ODFFormulaParser matchAll: '7' with: #unaryExpression) = 7. self assert: (ODFFormulaParser matchAll: '-7' with: #unaryExpression) = #(negated 7). self assert: (ODFFormulaParser matchAll: '-(1+2*sin(3))' with: #unaryExpression) = #(negated (+ 1 (* 2 (sin 3)))). self assert: (ODFFormulaParser parse: '7') = 7. self assert: (ODFFormulaParser parse: '-7') = #(negated 7). self assert: (ODFFormulaParser parse: '3 + 4') = #(+ 3 4). self assert: (ODFFormulaParser parse: '( 7 )') = 7. self assert: (ODFFormulaParser parse: 'min ( 1 , 2 ) ') = #(min 1 2). self assert: (ODFFormulaParser parse: '?aA0 ') = #(f aA0). self assert: (ODFFormulaParser parse: '$999 ') = #(m 999). ! ! !ODFReaderTest methodsFor: 'testing' stamp: 'tak 4/7/2011 14:13'! testFormulaPathParser "self debug: #testFormulaPathParser" self assert: (ODFFormulaParser matchAll: '1 23' with: #path) asArray = {$1. $ . $2. $3}. self assert: (ODFFormulaParser matchAll: '1 $2 3 ' with: #path) asArray = {$1. $ . #(m 2). $3. $ }. self assert: (ODFFormulaParser matchAll: '1 ?func 3' with: #path) asArray = {$1. $ . #(f func). $3}. ! ! !ODFFormulaParser reorganize! ('public' path start) ('terminals' binaryFunction close comma functionReference identifier modifierReference open ternaryFunction unaryFunction) ('non terminals' additiveExpression basicExpression multiplicativeExpression number unaryExpression) ! !ODFFormulaInterpreter class reorganize! ('instance creation' modifiers:functions:viewBox:stretchpoint:style: newFrom:style: renderDrawEnhancedGeometry:style: renderDrawEquations:) ! !ODFFormulaInterpreter reorganize! ('eval' evalPath: eval:) ('expressions' bottom hasfill hasstroke height left logheight logwidth pi right top width xstretch ystretch) ('private' modifiers:functions:viewBox:stretchpoint:style:) !