xref: /trunk/main/filter/source/flash/swfwriter.cxx (revision 07a3d7f1)
1 /**************************************************************
2  *
3  * Licensed to the Apache Software Foundation (ASF) under one
4  * or more contributor license agreements.  See the NOTICE file
5  * distributed with this work for additional information
6  * regarding copyright ownership.  The ASF licenses this file
7  * to you under the Apache License, Version 2.0 (the
8  * "License"); you may not use this file except in compliance
9  * with the License.  You may obtain a copy of the License at
10  *
11  *   http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing,
14  * software distributed under the License is distributed on an
15  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16  * KIND, either express or implied.  See the License for the
17  * specific language governing permissions and limitations
18  * under the License.
19  *
20  *************************************************************/
21 
22 
23 
24 // MARKER(update_precomp.py): autogen include statement, do not remove
25 #include "precompiled_filter.hxx"
26 #include "swfwriter.hxx"
27 #include <vcl/virdev.hxx>
28 #include <vcl/gdimtf.hxx>
29 #include <basegfx/matrix/b2dhommatrixtools.hxx>
30 
31 using namespace ::swf;
32 using namespace ::std;
33 using namespace ::com::sun::star::uno;
34 using namespace ::com::sun::star::io;
35 
36 // -----------------------------------------------------------------------------
37 
38 static MapMode aTWIPSMode( MAP_TWIP );
39 static MapMode a100thmmMode( MAP_100TH_MM );
40 
map100thmm(sal_Int32 n100thMM)41 static sal_Int32 map100thmm( sal_Int32 n100thMM )
42 {
43 	Point aPoint( n100thMM, n100thMM );
44 	sal_Int32 nX = OutputDevice::LogicToLogic( aPoint,  a100thmmMode, aTWIPSMode ).X();
45 	return nX;
46 }
47 
48 // -----------------------------------------------------------------------------
49 
Writer(sal_Int32 nTWIPWidthOutput,sal_Int32 nTWIPHeightOutput,sal_Int32 nDocWidthInput,sal_Int32 nDocHeightInput,sal_Int32 nJPEGcompressMode)50 Writer::Writer( sal_Int32 nTWIPWidthOutput, sal_Int32 nTWIPHeightOutput, sal_Int32 nDocWidthInput, sal_Int32 nDocHeightInput, sal_Int32 nJPEGcompressMode )
51 :	mpClipPolyPolygon( NULL ),
52 	mpTag( NULL ),
53 	mpSprite( NULL ),
54 	mnNextId( 1 ),
55 	mnGlobalTransparency(0),
56 	mnJPEGCompressMode(nJPEGcompressMode)
57 {
58 	mpVDev = new VirtualDevice;
59 	mpVDev->EnableOutput( sal_False );
60 
61 	maMovieTempFile.EnableKillingFile();
62 	maFontsTempFile.EnableKillingFile();
63 
64 	mpMovieStream = maMovieTempFile.GetStream( STREAM_WRITE|STREAM_TRUNC );
65 	mpFontsStream = maFontsTempFile.GetStream( STREAM_WRITE|STREAM_TRUNC );
66 
67 	mnFrames = 0;
68 
69 	mnDocWidth = map100thmm( nDocWidthInput );
70 	mnDocHeight = map100thmm( nDocHeightInput );
71 
72 	mnDocXScale = (double)nTWIPWidthOutput / mnDocWidth;
73 	mnDocYScale = (double)nTWIPHeightOutput / mnDocHeight;
74 
75 #ifndef AUGUSTUS
76 	// define an invisible button with the size of a page
77 	Rectangle aRect( 0, 0, (long)( mnDocWidth * mnDocXScale ), (long)( mnDocHeight * mnDocYScale ) );
78 	Polygon aPoly( aRect );
79 	FillStyle aFill = FillStyle( Color(COL_WHITE) );
80 	mnWhiteBackgroundShapeId = defineShape( aPoly, aFill );
81 
82 	::basegfx::B2DHomMatrix m; // #i73264#
83 	mnPageButtonId = createID();
84 	startTag( TAG_DEFINEBUTTON );
85 	mpTag->addUI16( mnPageButtonId );			// character id for button
86 
87 	// button records
88 	mpTag->addUI8( 0x08 );						// only hit state
89 	mpTag->addUI16( mnWhiteBackgroundShapeId ); // shape id of background rectangle
90 	mpTag->addUI16( 0 );						// depth for button DANGER!
91 	mpTag->addMatrix( m );						// identity matrix
92 	mpTag->addUI8( 0 );							// empty color transform
93 
94 //	mpTag->addUI8( 0 );							// end of button records
95 
96 	// action records
97 	mpTag->addUI8( 0x06 );						// ActionPlay
98 	mpTag->addUI8( 0 );							// end of action records
99 
100 	endTag();
101 
102 	// place a shape that clips shapes depth 2-3 to document boundaries
103 //	placeShape( mnWhiteBackgroundShapeId, 1, 0, 0, 4 );
104 #endif
105 }
106 
107 // -----------------------------------------------------------------------------
108 
~Writer()109 Writer::~Writer()
110 {
111 	delete mpVDev;
112 	delete mpSprite;
113 	delete mpTag;
114 }
115 
116 // -----------------------------------------------------------------------------
117 
ImplCopySvStreamToXOutputStream(SvStream & rIn,Reference<XOutputStream> & xOut)118 void ImplCopySvStreamToXOutputStream( SvStream& rIn, Reference< XOutputStream > &xOut )
119 {
120 	sal_uInt32 nBufferSize = 64*1024;
121 
122 	rIn.Seek( STREAM_SEEK_TO_END );
123 	sal_uInt32 nSize = rIn.Tell();
124 	rIn.Seek( STREAM_SEEK_TO_BEGIN );
125 
126 	Sequence< sal_Int8 > aBuffer( min( nBufferSize, nSize ) );
127 
128 	while( nSize )
129 	{
130 		if( nSize < nBufferSize )
131 		{
132 			nBufferSize = nSize;
133 			aBuffer.realloc( nSize );
134 		}
135 
136 		sal_uInt32 nRead = rIn.Read( aBuffer.getArray(), nBufferSize );
137 		DBG_ASSERT( nRead == nBufferSize, "ImplCopySvStreamToXOutputStream failed!" );
138 		xOut->writeBytes( aBuffer );
139 
140 		if( nRead == 0 )
141 			break;
142 
143 		nSize -= nRead;
144 	}
145 }
146 
147 // -----------------------------------------------------------------------------
148 
storeTo(Reference<XOutputStream> & xOutStream)149 void Writer::storeTo( Reference< XOutputStream > &xOutStream )
150 {
151 	for(FontMap::iterator i = maFonts.begin(); i != maFonts.end(); i++)
152 	{
153 		FlashFont* pFont = (*i);
154 		pFont->write( *mpFontsStream );
155 		delete pFont;
156 	}
157 
158 	// Endtag
159 	*mpMovieStream << (sal_uInt16)0;
160 
161 	Tag aHeader( 0xff );
162 
163 	aHeader.addUI8( 'F' );
164 	aHeader.addUI8( 'W' );
165 	aHeader.addUI8( 'S' );
166 	aHeader.addUI8( 5 );
167 
168 	sal_uInt32 nSizePos = aHeader.Tell();
169 
170 	aHeader << (sal_uInt32)0;
171 
172 	Rectangle aDocRect( 0, 0, static_cast<long>(mnDocWidth*mnDocXScale), static_cast<long>(mnDocHeight*mnDocYScale) );
173 
174 	aHeader.addRect( aDocRect );
175 
176 	// frame delay in 8.8 fixed number of frames per second
177 	aHeader.addUI8( 0 );
178 	aHeader.addUI8( 12 );
179 
180 	aHeader.addUI16( _uInt16(mnFrames) );
181 
182 	const sal_uInt32 nSize = aHeader.Tell() + mpFontsStream->Tell() + mpMovieStream->Tell();
183 
184 	aHeader.Seek( nSizePos );
185 	aHeader << (sal_uInt32)nSize;
186 
187 	ImplCopySvStreamToXOutputStream( aHeader, xOutStream );
188 	ImplCopySvStreamToXOutputStream( *mpFontsStream, xOutStream );
189 	ImplCopySvStreamToXOutputStream( *mpMovieStream, xOutStream );
190 }
191 
192 // -----------------------------------------------------------------------------
193 
startSprite()194 sal_uInt16 Writer::startSprite()
195 {
196 	sal_uInt16 nShapeId = createID();
197 	mvSpriteStack.push(mpSprite);
198 	mpSprite = new Sprite( nShapeId );
199 	return nShapeId;
200 }
201 
202 // -----------------------------------------------------------------------------
203 
endSprite()204 void Writer::endSprite()
205 {
206 	if( mpSprite )
207 	{
208 		startTag( TAG_END );
209 		endTag();
210 
211 		mpSprite->write( *mpMovieStream );
212 		delete mpSprite;
213 
214 		if (mvSpriteStack.size() > 0)
215 		{
216 			mpSprite = mvSpriteStack.top();
217 			mvSpriteStack.pop();
218 		}
219 		else
220 			mpSprite = NULL;
221 	}
222 }
223 
224 // -----------------------------------------------------------------------------
225 
placeShape(sal_uInt16 nID,sal_uInt16 nDepth,sal_Int32 x,sal_Int32 y,sal_uInt16 nClip,const char * pName)226 void Writer::placeShape( sal_uInt16 nID, sal_uInt16 nDepth, sal_Int32 x, sal_Int32 y, sal_uInt16 nClip, const char* pName )
227 {
228 	startTag( TAG_PLACEOBJECT2 );
229 
230 	BitStream aBits;
231 
232 	aBits.writeUB( nClip != 0, 1 );		// Has Clip Actions?
233 	aBits.writeUB( 0, 1 );				// reserved
234 	aBits.writeUB( pName != NULL, 1 );	// has a name
235 	aBits.writeUB( 0, 1 );				// no ratio
236 	aBits.writeUB( 0, 1 );				// no color transform
237 	aBits.writeUB( 1, 1 );				// has a matrix
238 	aBits.writeUB( 1, 1 );				// places a character
239 	aBits.writeUB( 0, 1 );				// does not define a character to be moved
240 
241 	mpTag->addBits( aBits );
242 	mpTag->addUI16( nDepth );		// depth
243 	mpTag->addUI16( nID );			// character Id
244 
245     // #i73264#
246     const basegfx::B2DHomMatrix aMatrix(basegfx::tools::createTranslateB2DHomMatrix(
247         _Int16(static_cast<long>(map100thmm(x)*mnDocXScale)),
248         _Int16(static_cast<long>(map100thmm(y)*mnDocYScale))));
249 	mpTag->addMatrix( aMatrix );		// transformation matrix
250 
251 	if( pName )
252 		mpTag->addString( pName );
253 
254 	if( nClip != 0 )
255 		mpTag->addUI16( nClip );
256 
257 	endTag();
258 }
259 
260 #ifdef THEFUTURE
261 // -----------------------------------------------------------------------------
262 
moveShape(sal_uInt16 nDepth,sal_Int32 x,sal_Int32 y)263 void Writer::moveShape( sal_uInt16 nDepth, sal_Int32 x, sal_Int32 y )
264 {
265 	startTag( TAG_PLACEOBJECT2 );
266 
267 	BitStream aBits;
268 	aBits.writeUB( 0, 1 );				// Has no Clip Actions
269 	aBits.writeUB( 0, 1 );				// reserved
270 	aBits.writeUB( 0, 1 );				// has no name
271 	aBits.writeUB( 0, 1 );				// no ratio
272 	aBits.writeUB( 0, 1 );				// no color transform
273 	aBits.writeUB( 1, 1 );				// has a matrix
274 	aBits.writeUB( 0, 1 );				// places a character
275 	aBits.writeUB( 1, 1 );				// defines a character to be moved
276 
277 	mpTag->addBits( aBits );
278 	mpTag->addUI16( nDepth );			// depth
279 
280     // #i73264#
281     const basegfx::B2DHomMatrix aMatrix(basegfx::tools::createTranslateB2DHomMatrix(
282         _Int16(static_cast<long>(map100thmm(x)*mnDocXScale)),
283         _Int16(static_cast<long>(map100thmm(y)*mnDocYScale))));
284 	mpTag->addMatrix( aMatrix );		// transformation matrix
285 
286 	endTag();
287 }
288 #endif
289 
290 // -----------------------------------------------------------------------------
291 
removeShape(sal_uInt16 nDepth)292 void Writer::removeShape( sal_uInt16 nDepth )
293 {
294 	startTag( TAG_REMOVEOBJECT2 );
295 	mpTag->addUI16( nDepth );			// depth
296 	endTag();
297 }
298 
299 // -----------------------------------------------------------------------------
300 
startTag(sal_uInt8 nTagId)301 void Writer::startTag( sal_uInt8 nTagId )
302 {
303 	DBG_ASSERT( mpTag == NULL, "Last tag was not ended");
304 
305 	mpTag = new Tag( nTagId );
306 }
307 
308 // -----------------------------------------------------------------------------
309 
endTag()310 void Writer::endTag()
311 {
312 	sal_uInt8 nTag = mpTag->getTagId();
313 
314 	if( mpSprite && ( (nTag == TAG_END) || (nTag == TAG_SHOWFRAME) || (nTag == TAG_DOACTION) || (nTag == TAG_STARTSOUND) || (nTag == TAG_PLACEOBJECT) || (nTag == TAG_PLACEOBJECT2) || (nTag == TAG_REMOVEOBJECT2) || (nTag == TAG_FRAMELABEL) ) )
315 	{
316 		mpSprite->addTag( mpTag );
317 		mpTag = NULL;
318 	}
319 	else
320 	{
321 		mpTag->write( *mpMovieStream );
322 		delete mpTag;
323 		mpTag = NULL;
324 	}
325 }
326 
327 // -----------------------------------------------------------------------------
328 
createID()329 sal_uInt16 Writer::createID()
330 {
331 	return mnNextId++;
332 }
333 
334 // -----------------------------------------------------------------------------
335 
showFrame()336 void Writer::showFrame()
337 {
338 	startTag( TAG_SHOWFRAME );
339 	endTag();
340 
341 	if(NULL == mpSprite)
342 		mnFrames++;
343 }
344 
345 // -----------------------------------------------------------------------------
346 
defineShape(const GDIMetaFile & rMtf,sal_Int16 x,sal_Int16 y)347 sal_uInt16 Writer::defineShape( const GDIMetaFile& rMtf, sal_Int16 x, sal_Int16 y )
348 {
349 	mpVDev->SetMapMode( rMtf.GetPrefMapMode() );
350 	Impl_writeActions( rMtf );
351 
352 	sal_uInt16 nId = 0;
353 	sal_uInt16 iDepth = 1;
354 	{
355 		CharacterIdVector::iterator aIter( maShapeIds.begin() );
356 		const CharacterIdVector::iterator aEnd( maShapeIds.end() );
357 
358 		sal_Bool bHaveShapes = aIter != aEnd;
359 
360 		if (bHaveShapes)
361 		{
362 			nId = startSprite();
363 
364 			while( aIter != aEnd )
365 			{
366 				placeShape( *aIter, iDepth++, x, y );
367 				aIter++;
368 			}
369 
370 			endSprite();
371 		}
372 	}
373 
374 	maShapeIds.clear();
375 
376 	return nId;
377 }
378 
379 // -----------------------------------------------------------------------------
380 
defineShape(const Polygon & rPoly,const FillStyle & rFillStyle)381 sal_uInt16 Writer::defineShape( const Polygon& rPoly, const FillStyle& rFillStyle )
382 {
383 	const PolyPolygon aPolyPoly( rPoly );
384 	return defineShape( aPolyPoly, rFillStyle );
385 }
386 
387 // -----------------------------------------------------------------------------
388 
defineShape(const PolyPolygon & rPolyPoly,const FillStyle & rFillStyle)389 sal_uInt16 Writer::defineShape( const PolyPolygon& rPolyPoly, const FillStyle& rFillStyle )
390 {
391 	sal_uInt16 nShapeId = createID();
392 
393 	// start a DefineShape3 tag
394 	startTag( TAG_DEFINESHAPE3 );
395 
396 	mpTag->addUI16( nShapeId );
397 	mpTag->addRect( rPolyPoly.GetBoundRect() );
398 
399 
400 	// FILLSTYLEARRAY
401 	mpTag->addUI8( 1 );			// FillStyleCount
402 
403 	// FILLSTYLE
404 	rFillStyle.addTo( mpTag );
405 
406 	// LINESTYLEARRAY
407 	mpTag->addUI8( 0 );			// LineStyleCount
408 
409 	// Number of fill and line index bits to 1
410 	mpTag->addUI8( 0x11 );
411 
412 	BitStream aBits;
413 
414 	const sal_uInt16 nCount = rPolyPoly.Count();
415 	sal_uInt16 i;
416 	for( i = 0; i < nCount; i++ )
417 	{
418 		const Polygon& rPoly = rPolyPoly[ i ];
419 		if( rPoly.GetSize() )
420 			Impl_addPolygon( aBits, rPoly, true );
421 	}
422 
423 	Impl_addEndShapeRecord( aBits );
424 
425 	mpTag->addBits( aBits );
426 	endTag();
427 
428 	return nShapeId;
429 }
430 
431 // -----------------------------------------------------------------------------
432 
defineShape(const PolyPolygon & rPolyPoly,sal_uInt16 nLineWidth,const Color & rLineColor)433 sal_uInt16 Writer::defineShape( const PolyPolygon& rPolyPoly, sal_uInt16 nLineWidth, const Color& rLineColor )
434 {
435 	sal_uInt16 nShapeId = createID();
436 
437 	// start a DefineShape3 tag
438 	startTag( TAG_DEFINESHAPE3 );
439 
440 	mpTag->addUI16( nShapeId );
441 	mpTag->addRect( rPolyPoly.GetBoundRect() );
442 
443 
444 	// FILLSTYLEARRAY
445 	mpTag->addUI8( 0 );			// FillStyleCount
446 
447 	// LINESTYLEARRAY
448 	mpTag->addUI8( 1 );			// LineStyleCount
449 
450 	// LINESTYLE
451 	mpTag->addUI16( nLineWidth );	// Width of line in twips
452 	mpTag->addRGBA( rLineColor );	// Color
453 
454 	// Number of fill and line index bits to 1
455 	mpTag->addUI8( 0x11 );
456 
457 	BitStream aBits;
458 
459 	const sal_uInt16 nCount = rPolyPoly.Count();
460 	sal_uInt16 i;
461 	for( i = 0; i < nCount; i++ )
462 	{
463 		const Polygon& rPoly = rPolyPoly[ i ];
464 		if( rPoly.GetSize() )
465 			Impl_addPolygon( aBits, rPoly, false );
466 	}
467 
468 	Impl_addEndShapeRecord( aBits );
469 
470 	mpTag->addBits( aBits );
471 	endTag();
472 
473 	return nShapeId;
474 }
475 
476 #ifdef AUGUSTUS
477 enum {NO_COMPRESSION, ADPCM_COMPRESSION, MP3_COMPRESSION } COMPRESSION_TYPE;
streamSound(const char * filename)478 sal_Bool Writer::streamSound( const char * filename )
479 {
480 	SF_INFO      info;
481 	SNDFILE *sf = sf_open(filename, SFM_READ, &info);
482 
483 	if (NULL == sf)
484 		return sal_False;
485 	else
486 	{
487 		// AS: Start up lame.
488 		m_lame_flags = lame_init();
489 
490 		// The default (if you set nothing) is a a J-Stereo, 44.1khz
491 		// 128kbps CBR mp3 file at quality 5.  Override various default settings
492 		// as necessary, for example:
493 
494 		lame_set_num_channels(m_lame_flags,1);
495 		lame_set_in_samplerate(m_lame_flags,22050);
496 		lame_set_brate(m_lame_flags,48);
497 		lame_set_mode(m_lame_flags,MONO);
498 		lame_set_quality(m_lame_flags,2);   /* 2=high  5 = medium  7=low */
499 
500 		// See lame.h for the complete list of options.  Note that there are
501 		// some lame_set_*() calls not documented in lame.h.  These functions
502 		// are experimental and for testing only.  They may be removed in
503 		// the future.
504 
505 		//4. Set more internal configuration based on data provided above,
506 		//   as well as checking for problems.  Check that ret_code >= 0.
507 
508 		int ret_code = lame_init_params(m_lame_flags);
509 
510 		if (ret_code < 0)
511 			throw 0;
512 
513 		int lame_frame_size = lame_get_framesize(m_lame_flags);
514 		int samples_per_frame = 22050 / 12; // AS: (samples/sec) / (frames/sec) = samples/frame
515 		int mp3buffer_size = static_cast<int>(samples_per_frame*1.25 + 7200 + 7200);
516 
517 
518 		startTag(TAG_SOUNDSTREAMHEAD2);
519 
520 		mpTag->addUI8(2<<2 | 1<<1 | 0<<0);  // Preferred mixer format ??
521 
522 		BitStream bs;
523 
524 		bs.writeUB(MP3_COMPRESSION,4);
525 		bs.writeUB(2, 2);  // AS: Reserved zero bits.
526 		bs.writeUB(1, 1);  // AS: 16 Bit
527 		bs.writeUB(0, 1);  // AS: Mono.
528 
529 		mpTag->addBits(bs);
530 
531 		mpTag->addUI16(samples_per_frame);
532 		endTag();
533 
534 		short *sample_buff = new short[static_cast<int>(info.frames)];
535 		sf_readf_short(sf, sample_buff, info.frames);
536 
537 		unsigned char* mp3buffer = new unsigned char[mp3buffer_size];
538 
539 // 5. Encode some data.  input pcm data, output (maybe) mp3 frames.
540 // This routine handles all buffering, resampling and filtering for you.
541 // The required mp3buffer_size can be computed from num_samples,
542 // samplerate and encoding rate, but here is a worst case estimate:
543 // mp3buffer_size (in bytes) = 1.25*num_samples + 7200.
544 // num_samples = the number of PCM samples in each channel.  It is
545 // not the sum of the number of samples in the L and R channels.
546 //
547 // The return code = number of bytes output in mp3buffer.  This can be 0.
548 // If it is <0, an error occurred.
549 
550 
551 		for (int samples_written = 0; samples_written < info.frames; samples_written += samples_per_frame)
552 		{
553 			startTag(TAG_SOUNDSTREAMBLOCK);
554 
555 			int samples_to_write = std::min((int)info.frames - samples_written, samples_per_frame);
556 
557 			// AS: Since we're mono, left and right sample buffs are the same
558 			//  ie, samplebuff (which is why we pass it twice).
559 			int ret = lame_encode_buffer(m_lame_flags, sample_buff + samples_written,
560 											sample_buff + samples_written,
561 											samples_to_write, mp3buffer, mp3buffer_size);
562 
563 			if (ret < 0)
564 				throw 0;
565 
566 // 6. lame_encode_flush will flush the buffers and may return a
567 // final few mp3 frames.  mp3buffer should be at least 7200 bytes.
568 // return code = number of bytes output to mp3buffer.  This can be 0.
569 
570 			if (mp3buffer_size - ret < 7200)
571 				throw 0;
572 
573 			int ret2 = lame_encode_flush(m_lame_flags, mp3buffer + ret, mp3buffer_size - ret);
574 
575 			if (ret2 < 0)
576 				throw 0;
577 
578 
579 			SvMemoryStream strm(mp3buffer, ret + ret2, STREAM_READWRITE);
580 
581 			mpTag->addUI16(samples_to_write); //lame_frame_size);
582 			mpTag->addUI16(0);
583 			mpTag->addStream(strm);
584 
585 			endTag();
586 
587 			showFrame();
588 		}
589 
590 
591 		delete[] mp3buffer;
592 
593 		delete[] sample_buff;
594 		int err = sf_close(sf);
595 
596 		// 8. free the internal data structures.
597 		lame_close(m_lame_flags);
598 	}
599 
600 	return sal_True;
601 }
602 #endif // AUGUSTUS
603 
604 
605 // -----------------------------------------------------------------------------
606 
stop()607 void Writer::stop()
608 {
609 	startTag( TAG_DOACTION );
610 	mpTag->addUI8( 0x07 );
611 	mpTag->addUI8( 0 );
612 	endTag();
613 }
614 
615 // -----------------------------------------------------------------------------
616 
waitOnClick(sal_uInt16 nDepth)617 void Writer::waitOnClick( sal_uInt16 nDepth )
618 {
619 	placeShape( _uInt16( mnPageButtonId ), nDepth, 0, 0 );
620 	stop();
621 	showFrame();
622 	removeShape( nDepth );
623 }
624 
625 // -----------------------------------------------------------------------------
626 
627 /** inserts a doaction tag with an ActionGotoFrame */
gotoFrame(sal_uInt16 nFrame)628 void Writer::gotoFrame( sal_uInt16 nFrame )
629 {
630 	startTag( TAG_DOACTION );
631 	mpTag->addUI8( 0x81 );
632 	mpTag->addUI16( 2 );
633 	mpTag->addUI16( nFrame );
634 	mpTag->addUI8( 0 );
635 	endTag();
636 }
637