DIRECTORY CGCubic USING [Bezier, CoeffsToBezier, Coeffs], GFileFormatDefs, PrincOpsUtils USING [LongCOPY], Basics USING [BytePair], Graphics USING [Path, MoveTo, LineTo, CurveTo, NewPath], CGPath USING [Empty], IO, FS, PairList, ReadGriffin, RealConvert USING [Mesa5ToIeee], Rope, SplineDefs, StyleDefs, GraphicsBasic USING [Vec]; ReadGriffinImpl: CEDAR PROGRAM IMPORTS CGCubic, PrincOpsUtils, IO, PairList, ReadGriffin, RealConvert, Rope, SplineDefs, FS, Graphics, CGPath EXPORTS ReadGriffin = { OPEN ReadGriffin; ROPE: TYPE = Rope.ROPE; GriffinFileError: PUBLIC SIGNAL[why: ROPE] = CODE; FigureNumber: TYPE = CARDINAL[1 .. 100]; IntegerVec: TYPE = RECORD[ x, y: INTEGER]; fontMap: PUBLIC PairList.Relation; -- styleMap: PUBLIC PairList.Relation; -- objectMap: PUBLIC PairList.Relation; -- pathMap: PUBLIC PairList.Relation; -- captionMap: PUBLIC PairList.Relation; -- clusterMap: PUBLIC PairList.Relation; -- objectsReversed: BOOLEAN _ FALSE; firstCluster, lastCluster: PUBLIC CARDINAL _ 0; colors: PUBLIC ColorRef _ NIL; colorNumber: PUBLIC CARDINAL _ 0; pathStyles: PUBLIC PathStyleRef _ NIL; pathStyleNumber: PUBLIC CARDINAL _ 0; captionStyles: PUBLIC CaptionStyleRef _ NIL; captionStyleNumber: PUBLIC CARDINAL _ 0; objectNumber: CARDINAL _ 1; -- count of objects in this view inThisView: BOOLEAN _ FALSE; diskHandle: IO.STREAM; maxCPairCount: CARDINAL = 2000; -- just for safety majorVersion: GFileFormatDefs.BYTE = 1; minorVersion: GFileFormatDefs.BYTE = 4; realConvert: BOOLEAN _ FALSE; -- convert reals if older than Griffin version 1.4 ConvertFile: PUBLIC PROCEDURE[fileName: ROPE, view: ViewType _ main] = { IF view = both THEN view _ main; fileName _ FixFileName[fileName, ".Griffin"]; diskHandle _ FS.StreamOpen[fileName ! FS.Error => CHECKED {GOTO BadCreate}]; InitDataStructures[]; ReadFigure[1, view]; IF ~objectsReversed THEN { clusterMap _ ReverseRelation[clusterMap]; objectsReversed _ TRUE}; IO.Close[diskHandle]; EXITS BadCreate => {SIGNAL GriffinFileError[Rope.Cat["file ", fileName, " not found"]]}; }; InitDataStructures: PROCEDURE = { colors _ InitColors[]; fontMap _ PairList.CreateRelation[]; styleMap _ PairList.CreateRelation[]; objectMap _ PairList.CreateRelation[]; pathMap _ PairList.CreateRelation[]; captionMap _ PairList.CreateRelation[]; clusterMap _ PairList.CreateRelation[]; objectsReversed _ FALSE; firstCluster _ 0; lastCluster _ 0; pathStyles _ NIL; pathStyleNumber _ 0; captionStyles _ NIL; captionStyleNumber _ 0; }; DestroyDataStructures: PROCEDURE = { PairList.DestroyRelation[fontMap]; PairList.DestroyRelation[styleMap]; PairList.DestroyRelation[objectMap]; PairList.DestroyRelation[pathMap]; PairList.DestroyRelation[captionMap]; PairList.DestroyRelation[clusterMap]; pathStyles _ NIL; captionStyles _ NIL; colors _ NIL; }; ReadFigure: PROCEDURE[fignum: FigureNumber, view: ViewType] = TRUSTED { fileheader: GFileFormatDefs.GFileHeader; hcontrol: GFileFormatDefs.GFileHardcopyController; dcontrol: GFileFormatDefs.GFileDisplayController; figurename: GFileFormatDefs.GFileFigureName; fontCount, styleCount, objectCount, f, s: CARDINAL; ReadHeader[@fileheader]; MoveToSector[fileheader.figure[fignum]]; -- position to the right sector ReadStructure[@figurename, GFileFormatDefs.lGFileFigureName]; ReadStructure[@hcontrol, GFileFormatDefs.lGFileHardcopyController]; ReadStructure[@dcontrol, GFileFormatDefs.lGFileDisplayController]; IF dcontrol.numcontrolpairs > maxCPairCount THEN SIGNAL GriffinFileError["too many control pairs"]; fontCount _ ReadWord[]; FOR f IN [1 .. fontCount] DO ReadFont[f, fontMap]; ENDLOOP; styleCount _ ReadWord[]; FOR s IN [1 .. styleCount] DO ReadStyle[s, styleMap]; ENDLOOP; objectCount _ ReadWord[]; THROUGH [1 .. objectCount] DO ReadObject[objectMap, view]; ENDLOOP; }; ReadHeader: PROCEDURE[h: POINTER TO GFileFormatDefs.GFileHeader] = TRUSTED { IO.SetIndex[diskHandle,0]; ReadStructure[h, GFileFormatDefs.lGFileHeader]; realConvert _ FALSE; IF h.majversion#majorVersion OR h.minversion#minorVersion THEN { IF h.majversion=1 AND h.minversion IN [0..3] THEN realConvert _ TRUE; }; }; ReadFont: PROCEDURE[fontNumber: CARDINAL, fontMap: PairList.Relation] = TRUSTED { font: FontRef _ NEW[GFileFormatDefs.GFileFont]; refFontId: REF CARDINAL _ NEW[CARDINAL _ fontNumber]; ReadStructure[LOOPHOLE[font], GFileFormatDefs.lGFileFont]; PairList.AddPair[fontMap, refFontId, font]; }; ReadStyle: PROCEDURE[styleNumber: CARDINAL, styleMap: PairList.Relation] = TRUSTED { style: StyleRef _ NEW[GFileFormatDefs.GFileStyle]; refStyleId: REF CARDINAL _ NEW[CARDINAL _ styleNumber]; ReadStructure[LOOPHOLE[style], GFileFormatDefs.lGFileStyle]; style.thickness _ GetReal[style.thickness]; PairList.AddPair[styleMap, refStyleId, style]; }; ReadObject: PROCEDURE[objectMap: PairList.Relation, view: ViewType] = TRUSTED { fileObjectRef: ObjectRef _ NEW[GFileFormatDefs.GFileObject]; topScreenCoord: INTEGER = 808; origin: IntegerVec; path: PathRef _ Graphics.NewPath[]; refCaption: CaptionRef _ NIL; ReadStructure[LOOPHOLE[fileObjectRef], GFileFormatDefs.lGFileObject]; inThisView _ ((view=main) AND (~fileObjectRef^.hidewindow)) OR ((view=alternate) AND (fileObjectRef^.hidewindow)); origin.x _ fileObjectRef^.bleft; origin.y _ topScreenCoord - fileObjectRef^.bbottom; SELECT fileObjectRef^.objtype FROM GFileFormatDefs.typeCurveObject, GFileFormatDefs.typeAreaObject => { nlinks: CARDINAL _ ReadWord[]; splinetype: SplineDefs.SplineType _ SELECT fileObjectRef^.splinetype FROM GFileFormatDefs.typeNUMSpline => naturalUM, GFileFormatDefs.typeCUMSpline => cyclicUM, GFileFormatDefs.typeNALSpline => naturalAL, GFileFormatDefs.typeCALSpline => cyclicAL, GFileFormatDefs.typeBEZSpline => bezier, GFileFormatDefs.typeBSISpline => bsplineInterp, GFileFormatDefs.typeBSpline => bspline, GFileFormatDefs.typeCRSpline => crspline, ENDCASE => bspline; SELECT fileObjectRef^.trajtype FROM GFileFormatDefs.typeLinkedTraj => AppendPathLinks[nlinks, splinetype, FALSE, origin, path]; GFileFormatDefs.typeCSTraj => { IF nlinks#1 THEN SIGNAL GriffinFileError["imposible cyclic spline"]; splinetype _ (SELECT splinetype FROM naturalUM => cyclicUM, naturalAL => cyclicAL, ENDCASE => splinetype); AppendPathLinks[1, splinetype, TRUE, origin, path]; }; ENDCASE => SIGNAL GriffinFileError["invalid trajectory"]; }; GFileFormatDefs.typeCaptionObject => { fcaptiontrailer: GFileFormatDefs.GFileCaptionTrailer; captionText: ROPE; pt: GraphicsBasic.Vec; ReadStructure[@fcaptiontrailer, GFileFormatDefs.lGFileCaptionTrailer]; pt.x _ GetReal[fcaptiontrailer.xanchor]; pt.y _ GetReal[fcaptiontrailer.yanchor]; pt.x _ ScalePressToScreenCoord[pt.x]; pt.y _ ScalePressToScreenCoord[pt.y]; captionText _ ReadString[]; refCaption _ NEW[CaptionRec _ [pt, captionText]]; }; ENDCASE => SIGNAL GriffinFileError["bad object"]; IF inThisView THEN { refObjectId: REF CARDINAL _ NEW[CARDINAL _ objectNumber]; refClusterId: REF CARDINAL _ NEW[CARDINAL _ fileObjectRef^.cluster]; PairList.AddPair[objectMap, refObjectId, fileObjectRef]; objectNumber _ objectNumber+1; SELECT fileObjectRef^.objtype FROM GFileFormatDefs.typeCurveObject, GFileFormatDefs.typeAreaObject => PairList.AddPair[pathMap, refObjectId, path]; GFileFormatDefs.typeCaptionObject => PairList.AddPair[captionMap, refObjectId, refCaption]; ENDCASE; firstCluster _ MIN[firstCluster, fileObjectRef^.cluster]; lastCluster _ MAX[lastCluster, fileObjectRef^.cluster]; PairList.AddPair[clusterMap, refClusterId, refObjectId]; }; }; AppendPathLinks: PROCEDURE[nlinks: CARDINAL, splinetype: SplineDefs.SplineType, cyclic: BOOLEAN, origin: IntegerVec, path: PathRef] = { THROUGH [1 .. nlinks] DO AddLink[splinetype, cyclic, origin, path]; ENDLOOP; }; AddLink: PROCEDURE[splinetype: SplineDefs.SplineType, cyclic: BOOLEAN, origin: IntegerVec, path: PathRef] = TRUSTED { knotword: GFileFormatDefs.GFileKnotWord; knots: SplineDefs.KnotSequence; coeffs: SplineDefs.CoeffsSequence; CoeffsLength: PROCEDURE [coeffs: SplineDefs.CoeffsSequence] RETURNS [CARDINAL] = TRUSTED { RETURN[IF coeffs=NIL THEN 0 ELSE coeffs.length] }; num, j: CARDINAL; fpoint: GFileFormatDefs.GFilePoint; ReadStructure[@knotword, GFileFormatDefs.lGFileKnotWord]; num _ knotword.knotcount; IF num=0 THEN SIGNAL GriffinFileError["bad link"]; SELECT knotword.knottype FROM GFileFormatDefs.typeFD3Knot => { knots _ NEW[SplineDefs.KnotSequenceRec[num]]; FOR j IN [0 .. num) DO ReadStructure[@fpoint, GFileFormatDefs.lGFilePoint]; knots[j] _ SplineDefs.FPCoords[GetReal[fpoint.x], GetReal[fpoint.y]]; ENDLOOP; IF cyclic THEN knots _ MakeCyclicKnots[knots,splinetype]; coeffs _ SplineDefs.MakeSpline[knots,splinetype]; FOR j IN [0..CoeffsLength[coeffs]) DO c: CGCubic.Coeffs; c.c0 _ [coeffs[j].t0[SplineDefs.X],coeffs[j].t0[SplineDefs.Y]]; c.c1 _ [coeffs[j].t1[SplineDefs.X],coeffs[j].t1[SplineDefs.Y]]; c.c2 _ [coeffs[j].t2[SplineDefs.X],coeffs[j].t2[SplineDefs.Y]]; c.c3 _ [coeffs[j].t3[SplineDefs.X],coeffs[j].t3[SplineDefs.Y]]; IF inThisView THEN AppendCubic[CGCubic.CoeffsToBezier[c], origin, path]; ENDLOOP; }; GFileFormatDefs.typeFD0Knot, GFileFormatDefs.typeFD1Knot, GFileFormatDefs.typeFD2Knot => FOR j IN [0 .. num) DO ReadStructure[@fpoint, GFileFormatDefs.lGFilePoint]; IF inThisView THEN AppendPoint[[GetReal[fpoint.x], GetReal[fpoint.y]], origin, path]; ENDLOOP; ENDCASE => SIGNAL GriffinFileError["bad link"]; }; MakeCyclicKnots: PROCEDURE[knots: SplineDefs.KnotSequence, type: SplineDefs.SplineType] RETURNS[SplineDefs.KnotSequence] = { cycknots: SplineDefs.KnotSequence; numknots: INTEGER _ IF knots=NIL THEN 0 ELSE knots.length; newLength,i: INTEGER; IF numknots <= 0 THEN RETURN[NIL]; SELECT type FROM cyclicUM, cyclicAL, bezier=> newLength _ numknots+1; bspline, crspline=> newLength _ numknots+3; ENDCASE; cycknots _ NEW[SplineDefs.KnotSequenceRec[newLength]]; FOR i IN [0..numknots) DO cycknots[i] _ knots[i]; ENDLOOP; cycknots[numknots] _ cycknots[0]; SELECT type FROM naturalUM => type _ cyclicUM; naturalAL => type _ cyclicAL; bspline => { cycknots[numknots+1] _ cycknots[1]; cycknots[numknots+2] _ cycknots[2]; }; crspline => { cycknots[numknots+1] _ cycknots[1]; cycknots[numknots+2] _ cycknots[2]; }; ENDCASE; RETURN[cycknots]; }; AppendCubic: PROCEDURE[c: CGCubic.Bezier, origin: IntegerVec, path: PathRef] = { b0, b1, b2, b3: GraphicsBasic.Vec; b0 _ XFormPt[c.b0,origin]; b1 _ XFormPt[c.b1,origin]; b2 _ XFormPt[c.b2,origin]; b3 _ XFormPt[c.b3,origin]; IF CGPath.Empty[path] THEN Graphics.MoveTo[path,b0.x,b0.y]; Graphics.CurveTo[path,b1.x,b1.y,b2.x,b2.y,b3.x,b3.y]; }; AppendPoint: PROCEDURE[p: GraphicsBasic.Vec, origin: IntegerVec, path: PathRef] = { v: GraphicsBasic.Vec _ XFormPt[p,origin]; IF CGPath.Empty[path] THEN Graphics.MoveTo[path,v.x,v.y] ELSE Graphics.LineTo[path,v.x,v.y]; }; XFormPt: PROC [p: GraphicsBasic.Vec, origin: IntegerVec] RETURNS [GraphicsBasic.Vec] = { RETURN[[ScalePressToScreenCoord[p.x]-origin.x, ScalePressToScreenCoord[p.y]-origin.y]]; }; ReverseRelation: PROCEDURE[map: PairList.Relation] RETURNS[PairList.Relation] = { new: PairList.Relation _ PairList.CreateRelation[]; AddPair: PROCEDURE[leftPart, rightPart: REF ANY _ NIL] = { PairList.AddPair[new, leftPart, rightPart]}; IF map=NIL THEN RETURN[map]; PairList.ForAllPairs[map, AddPair]; map _ new; PairList.DestroyRelation[new]; RETURN[map]; }; ReadWord: PROCEDURE RETURNS[CARDINAL] = { high, low: CHARACTER; word: Basics.BytePair; high _ IO.GetChar[diskHandle]; low _ IO.GetChar[diskHandle]; word.high _ LOOPHOLE[high]; word.low _ LOOPHOLE[low]; RETURN[LOOPHOLE[word]]; }; ReadString: PROCEDURE RETURNS[r: ROPE] = { t: REF TEXT; length: CARDINAL; length _ LOOPHOLE[IO.GetChar[diskHandle], CARDINAL]; t _ NEW[TEXT[length]]; IF IO.GetBlock[diskHandle,t,0,length] # length THEN SIGNAL GriffinFileError["Possible disk problem"]; r _ Rope.FromRefText[t]; IF (length+1) MOD 2 # 0 THEN [] _ IO.GetChar[diskHandle]; }; charsPerPage: CARDINAL ~ 512; MoveToSector: PROCEDURE[si: GFileFormatDefs.SectorIndex] = { IO.SetIndex[diskHandle, INTEGER[si*charsPerPage]]; }; ReadStructure: PROCEDURE[p: LONG POINTER, l: CARDINAL] = TRUSTED { from: LONG POINTER; b: REF TEXT _ NEW[TEXT[l*2]]; IF IO.GetBlock[diskHandle, b, 0, 2*l] # 2*l THEN SIGNAL GriffinFileError["Possible disk problem"]; from _ LOOPHOLE[b,LONG POINTER]+2; --start of bytes PrincOpsUtils.LongCOPY[from,l,p]; }; GetReal: PROCEDURE[r: REAL] RETURNS[out: REAL] = TRUSTED { IF realConvert THEN RETURN[RealConvert.Mesa5ToIeee[r]] ELSE RETURN[r]; }; FixFileName: PUBLIC PROCEDURE[oldname, extension: ROPE] RETURNS [newname:ROPE] = { dotPosition: INTEGER _ Rope.Find[oldname, "."]; IF dotPosition < 0 THEN newname _ Rope.Cat[oldname, extension] ELSE newname _ Rope.Cat[Rope.Substr[oldname, 0, dotPosition], extension]; }; InitColors: PROCEDURE[] RETURNS[ColorRef] = { -- create the Griffin color table: "name", h, s, b colors _ NEW[ColorRec _ [NIL, "black", 0, 0, 0]]; colors _ NEW[ColorRec _ [colors, "darkBrown", 7, 255, 59]]; colors _ NEW[ColorRec _ [colors, "brown", 7, 255, 118]]; colors _ NEW[ColorRec _ [colors, "tan", 0, 131, 217]]; colors _ NEW[ColorRec _ [colors, "maroon", 234, 255, 79]]; colors _ NEW[ColorRec _ [colors, "darkRed", 0, 255, 160]]; colors _ NEW[ColorRec _ [colors, "red", 0, 255, 255]]; colors _ NEW[ColorRec _ [colors, "orange", 10, 255, 255]]; colors _ NEW[ColorRec _ [colors, "darkYellow", 25, 255, 255]]; colors _ NEW[ColorRec _ [colors, "yellow", 40, 255, 255]]; colors _ NEW[ColorRec _ [colors, "lightYellow", 40, 190, 255]]; colors _ NEW[ColorRec _ [colors, "darkGreen", 71, 255, 59]]; colors _ NEW[ColorRec _ [colors, "green", 76, 255, 255]]; colors _ NEW[ColorRec _ [colors, "lightGreen", 71, 193, 255]]; colors _ NEW[ColorRec _ [colors, "darkBlue", 150, 255, 170]]; colors _ NEW[ColorRec _ [colors, "blue", 148, 255, 255]]; colors _ NEW[ColorRec _ [colors, "lightBlue", 141, 150, 255]]; colors _ NEW[ColorRec _ [colors, "darkAqua", 107, 255, 98]]; colors _ NEW[ColorRec _ [colors, "aqua", 107, 224, 255]]; colors _ NEW[ColorRec _ [colors, "cyan", 120, 255, 255]]; colors _ NEW[ColorRec _ [colors, "darkPurple", 178, 255, 178]]; colors _ NEW[ColorRec _ [colors, "purple", 170, 224, 255]]; colors _ NEW[ColorRec _ [colors, "violet", 170, 131, 255]]; colors _ NEW[ColorRec _ [colors, "magenta", 200, 255, 255]]; colors _ NEW[ColorRec _ [colors, "pink", 206, 170, 255]]; colors _ NEW[ColorRec _ [colors, "darkGrey", 0, 0, 40]]; colors _ NEW[ColorRec _ [colors, "grey", 0, 0, 120]]; colors _ NEW[ColorRec _ [colors, "lightGrey", 0, 0, 200]]; colors _ NEW[ColorRec _ [colors, "paleGrey", 0, 0, 230]]; colors _ NEW[ColorRec _ [colors, "white", 0, 0, 255]]; RETURN[colors]; }; }. œReadGriffinImpl.mesa Read Griffin files and convert to Tioga Artwork files Maureen Stone May 5, 1983 4:52 pm Rick Beach, May 1, 1984 1:51:59 pm PDT now our own data structures GriffinToTANodes.PutClusters[FixFileName[fileName, ".Artwork"]]; GriffinToTANodes.PutStyles[FixFileName[fileName, ".Style"]]; DestroyDataStructures[]; control pairs not implemented version 1.4 is the first with the Ieee floating point format. we need to set up to convert the reals for older versions so ForAllPairs in generating objects does so in chronological order private low-level disk access procedures always read an even number of bytes: string length + string chars MOD 2 = 0 Ê y˜šœ™Jšœ5™5Jšœ!™!Jšœ&™&—J˜šÏk ˜ Jšœœ"˜/J˜Jšœœ ˜Jšœœ ˜Jšœ œ*˜8Jšœœ ˜Jšœ˜Jšœ˜J˜ Jšœ ˜ Jšœ œ˜ J˜J˜ J˜ Jšœœ˜J˜—šœ ˜JšœœM˜oJšœ˜Jšœ ˜—Jšœœœ˜J˜Jš œœœœœ˜3J˜Jšœœœ ˜(J˜šœ œœ˜Jšœœ˜J˜—Jšœ™Jšœ œÏc˜Jšœ œž˜DJšœ œž ˜GJšœœœ˜!Jšœœœ˜/Jšœœ œ˜Jšœ œœ˜!Jšœ œœ˜&Jšœœœ˜%Jšœœœ˜,Jšœœœ˜(J˜Jšœœž ˜˜XJ˜J˜—šŸœ œ˜!J˜J˜$J˜%J˜&J˜$J˜'J˜'Jšœœ˜J˜J˜Jšœ œ˜J˜Jšœœ˜J˜J˜J˜—šŸœ œ˜$J˜"J˜#J˜$J˜"J˜%J˜%Jšœ œ˜Jšœœ˜Jšœ œ˜ J˜J˜—šŸ œ œ)œ˜GJ˜(J˜2J˜1J˜,Jšœ*œ˜3J˜J˜Jšœ)ž˜HJ˜=J˜CJ˜BJšœ™Jšœ*œœ,˜cJ˜šœœ˜J˜Jšœ˜—J˜šœœ˜J˜Jšœ˜—J˜šœœ˜J˜Jšœ˜—J˜J˜—š Ÿ œ œœœ œ˜LJšœ˜J˜/Jšœœ˜šœœœ˜@Jšœ=™=Jšœ9™9šœœœ˜,Jšœœ˜—J˜—J˜J˜—šŸœ œ œ œ˜QJšœœ˜/š œ œœœœ˜5Jšœœ$˜:—J˜+J˜J˜—šŸ œ œœ!œ˜Tšœœ˜2š œ œœœœ˜7Jšœœ&˜<——J˜+J˜.J˜J˜—šŸ œ œ1œ˜OJšœœ˜œ˜|J˜"Jš œ œœœœœ˜:Jšœ œ˜Jšœœœœ˜"šœ˜J˜ J˜ J˜ J˜J˜"Jšœ˜—Jšœ œ(˜6šœœ˜J˜Jšœ˜—J˜!šœ˜J˜J˜˜ J˜#J˜#J˜—˜J˜#J˜#J˜—Jšœ˜—Jšœ ˜J˜J˜—šŸ œ œ:˜PJ˜"J˜J˜J˜J˜Jšœœ!˜;J˜5J˜J˜—šŸ œ œ=˜SJ˜)Jšœœ˜8Jšœ˜#J˜J˜—šŸœœ,œ˜XJšœQ˜WJ˜J˜—JšœC™CJ˜šŸœ œœ˜QJ˜3š Ÿœ œœœœ˜:J˜,—Jšœœœœ˜Jšœ#˜#J˜ J˜Jšœ˜ J˜J˜J˜—Jšœ(™(J˜šŸœ œœœ˜)Jšœ œ˜J˜Jšœœ˜Jšœœ˜Jšœ œœ˜5Jšœœ˜J˜J˜—šŸ œ œœœ˜+Jšœœœ˜ Jšœœ˜Jšœ œœœ˜4Jšœœœ ˜šœœ)˜.Jšœœ+˜6—J˜JšœK™KJšœ œœœ˜9J˜J˜—Jšœœ˜šŸ œ œ%˜œ;œ9œ=œ<œ:œ=œ;œ9œ9œ>œ;œ;œ;œ9œ7œ5œ9œ8œD˜–J˜J˜J˜—…—9°IÅ