ConvertTextMovie.c

// ConvertMovie makes a copy of a movie, converting its text tracks to 
// new text tracks with bit map representations of the text. The displayFlags
// parameter allows one to add new display flags (such as normally time consuming
// antialias and drop shadow) for the bit map images. The imageTrack parameter
// indicates whether to save an image of the entire text track or just the
// text box (as specified by the defaultTextBox field of the text descriptor).
// The spatial parameter is a pointer to a compression information data structure
// that specifies how to compress the text bit maps.
 
// Note that the resulting movie file (as specified by the dstSpec parameter) will
// need to be flattened if it contained any non-text tracks in order to make it
// self-contained.
 
 
#include <QuickDraw.h>
#include <Files.h>
#include <StandardFile.h>
#include <Movies.h>
#include <ImageCompression.h>
#include "ConvertTextMovie.h"
 
// Special Call to Text Media Handler
pascal ComponentResult GetMetrics(MediaHandler mh, Handle metrics)
 FIVEWORDINLINE(0x2F3C, 0x04, 0x108, 0x7000, 0xA82A);
 
 
#define     FailNil(a)          if ((a)==nil)   goto bail;
#define     FailOSErr(a)        if (err = a)    goto bail;
#define     FailMemErr(a)       {a; if (err = MemError()) goto bail;}
#define     FailMoviesErr(a)    {a; if (err = GetMoviesError()) goto bail;}
 
pascal OSErr ConvertAndAddTextTrack(Track srcTrack, Track dstTrack, long newDisplayFlags, Boolean imageTrack, SCSpatialSettings *spatial);
 
pascal void ConvertMovie(FSSpec *srcSpec, FSSpec *dstSpec, long newDisplayFlags, Boolean imageTrack, SCSpatialSettings *spatial)
{
    OSErr   err = 0;
    Movie dstMovie = 0;
    Movie srcMovie = 0;
    Handle trackDataRef = 0;
    Track srcTrack;
    Track dstTrack;
    Handle srcDataRef;
    OSType srcDataRefType;
    long srcDataRefAttributes;
    short resRefNum = 0;
    Fixed width,height;
    OSType mediaType;
    Media srcMedia;
    Media dstMedia;
    TimeValue selectionTime, selectionDuration;
    GWorldPtr gw, movieWorld = 0;
    GDHandle gd;
    short dstRef = 0;
    short srcRef;
    short srcResID;
    short dstResID = 0;
    Rect bounds;
    WindowPtr movieWindow = 0;
    short numTracks, trackNum;
    Point pt;
    
    GetGWorld(&gw, &gd);
    
    // Open the source movie file
    FailOSErr( OpenMovieFile(srcSpec, &srcRef, fsRdPerm));
    srcResID = 0;
    FailOSErr( NewMovieFromFile(&srcMovie, srcRef, &srcResID, 0, 0, 0));
    CloseMovieFile(srcRef);
    srcRef = 0;
    
    // Create a new movie file for the destination
    CreateMovieFile(dstSpec, 'TVOD', 0, createMovieFileDeleteCurFile, &dstRef, &dstMovie);
    AddMovieResource(dstMovie, dstRef, &dstResID, 0);
    
    GetMovieBox(srcMovie, &bounds);
    OffsetRect(&bounds, -bounds.left, -bounds.top);
    SetMovieBox(srcMovie, &bounds);
    
    // Image the movie into an offscreen buffer
    FailOSErr( NewGWorld(&movieWorld, spatial->depth, &bounds, 0L, 0L, useTempMem));
    SetMovieGWorld(srcMovie, movieWorld, 0);
    
    OffsetRect(&bounds, 100, 100);
    // Create a new window to which we can copy the movie image during the conversion
    movieWindow = NewCWindow(0, &bounds, (StringPtr) "\pConverting...", true, noGrowDocProc, (WindowPtr)-1, false, 0);
    AlignWindow(movieWindow, false, &bounds, nil);
    SetMovieActive(srcMovie, true);
    
    SetPort((GrafPtr)movieWindow);
    
    // Iterate through the tracks, copying non-text tracks and converting text tracks
    numTracks = GetMovieTrackCount(srcMovie);
    for (trackNum = 1; trackNum <= numTracks; trackNum++) {
        ClearMoviesStickyError();
        srcTrack =  GetMovieIndTrack(srcMovie, trackNum);
        srcMedia = GetTrackMedia(srcTrack);
        
        srcDataRef = nil;
            
        GetTrackDimensions(srcTrack, &width, &height);
        dstTrack = NewMovieTrack(dstMovie, width, height, GetTrackVolume(srcTrack));
    
        GetMediaHandlerDescription(srcMedia, &mediaType, 0L, 0L);
        
        if (mediaType == 'text') {
            // TEXT TRACK; CONVERT IT TO BITMAP ("BURNT TEXT") REPRESENTATION
            if (err = ConvertAndAddTextTrack(srcTrack, dstTrack, newDisplayFlags, imageTrack, spatial)) {
                //DebugStr((StringPtr) "\pError returned by ConvertAndAddTextTrack");
                goto bail;
            }
        } else {
            // NON-TEXT TRACK; COPY A REFERENCE
            // Obtain the data ref of the source media since we are making a reference copy of the track.
            // The user will need to flatten the resulting file if they want a stand alone movie file.
            err = GetMediaDataRef(srcMedia, 1, &srcDataRef, &srcDataRefType, &srcDataRefAttributes);
            dstMedia = NewTrackMedia( dstTrack, mediaType,
                        GetMediaTimeScale(srcMedia), srcDataRef, srcDataRefType);
            
            selectionTime = 0;
            selectionDuration = GetMovieDuration(srcMovie);
                
            InsertTrackSegment(srcTrack, dstTrack, selectionTime, selectionDuration, 0);
            CopyTrackSettings(srcTrack, dstTrack);
                
            DisposeHandle(srcDataRef);
            srcDataRef = 0;
        }
        if (err = GetMoviesStickyError()) goto bail;
    }
 
    UpdateMovieResource(dstMovie, dstRef, dstResID, 0);
    err = GetMoviesError();
 
bail:
    // Clean up
    SetGWorld(gw, gd);
    if (movieWorld) DisposeGWorld(movieWorld);
    if (srcRef) CloseMovieFile(srcRef);
    if (dstRef) CloseMovieFile(dstRef);
    if (srcMovie) DisposeMovie(srcMovie);
    if (dstMovie) DisposeMovie(dstMovie);
    if (movieWindow) DisposeWindow(movieWindow);
}
 
#define min(x,y) ((x) < (y)) ? (x) : (y)
#define dfDontHilite (1L << 30)
#define dfDontBitmap (1L << 29)
 
pascal OSErr ConvertAndAddTextTrack(Track srcTrack, Track dstTrack, long newDisplayFlags, Boolean imageTrack, SCSpatialSettings *spatial)
{
    OSErr err;
    Movie srcMovie = GetTrackMovie(srcTrack);
    Media textMedia;
    MediaHandler textMediaHandler;
    Media newTextMedia;
    short sampleFlags;
    Fixed width, height;
    RgnHandle trackRgn;
    long size, dataSize, newDataSize;
    TimeValue sampleTime, sampleDuration, thisTrackTime, nextTrackTime, interestingDuration, mediaTime;
    ImageDescriptionHandle idh = (ImageDescriptionHandle) NewHandleClear(sizeof(ImageDescription));
    Handle dataOut = NewHandle(4);
    TextDescriptionHandle tdh = (TextDescriptionHandle) NewHandleClear(sizeof(TextDescription));
    Ptr imageData = 0;
    Ptr descPtr;
    Ptr dataPtr;
    long    descIndex;
    Handle metrics = NewHandle(0);
    GWorldPtr movieGW;
    GDHandle movieGD;
    GWorldPtr saveGW;
    GDHandle saveGD;
    Rect trackRect;
    Rect textBox;
    long compressedDataSize;
    Rect    movieBox;
    WindowPtr movieWindow = 0;
    MatrixRecord trackMatrix, movieMatrix;
    long textLen;
    
    GetPort((GrafPtr*)&movieWindow);
    GetMovieBox(srcMovie, &movieBox);
 
    // Create new text media
    textMedia = GetTrackMedia(srcTrack);
    textMediaHandler = GetMediaHandler(textMedia);
    FailMoviesErr( newTextMedia = NewTrackMedia(dstTrack, 'text', GetMediaTimeScale(textMedia), 0L, 0L));
 
    // Copy info from source track
    err = CopyTrackSettings(srcTrack, dstTrack);
    FailMoviesErr( SetTrackLayer(dstTrack, GetTrackLayer(srcTrack)));
    
    // Find the first text sample
    thisTrackTime = GetTrackOffset(srcTrack);
    GetTrackNextInterestingTime(srcTrack, nextTimeMediaSample+nextTimeEdgeOK, thisTrackTime, kFix1, 
                &nextTrackTime, &interestingDuration);
    SetMovieTimeValue(srcMovie, thisTrackTime);
    
    // Get the track bounds and set up for compressing text image
    trackRgn = GetTrackSegmentDisplayBoundsRgn(srcTrack, 0, GetTrackDuration(srcTrack));
    if (!trackRgn || EmptyRgn(trackRgn)) {
        err = -1;
        goto bail;
    }
    trackRect = (*trackRgn)->rgnBBox;
    GetMovieGWorld(srcMovie, &movieGW, &movieGD);
    FailOSErr( GetMaxCompressionSize(movieGW->portPixMap, &trackRect, spatial->depth, spatial->spatialQuality, 
                spatial->codecType, 0, &size));
    FailMemErr( imageData = NewPtr(size));
    
    // Get track matrix so that when we compress the image we are looking 
    // in the right place in the offscreen buffer
    GetMovieMatrix(srcMovie, &movieMatrix);
    GetTrackMatrix(srcTrack, &trackMatrix);
    ConcatMatrix(&movieMatrix, &trackMatrix);
 
    FailOSErr( BeginMediaEdits(newTextMedia));
    while (thisTrackTime >= 0) {
        TimeValue dur;
        short addFlags = 0;
        // Get the current sample
        mediaTime = TrackTimeToMediaTime(thisTrackTime, srcTrack);
        FailOSErr( GetMediaSample(textMedia, dataOut, 0, &dataSize, mediaTime, &sampleTime, &sampleDuration, 
                (SampleDescriptionHandle) tdh, &descIndex, 0, 0, &sampleFlags));
        
        // Add in the new display flags (and set special dontHilite & dontBitmap flags)
        (*tdh)->displayFlags |= (newDisplayFlags + dfDontHilite + dfDontBitmap);
        err = SetMediaSampleDescription(textMedia, descIndex, (SampleDescriptionHandle) tdh);
        
        SetMovieTimeValue(srcMovie, thisTrackTime);
 
        newDataSize = dataSize;
 
        // Get the track bounds for this time; if imageTrack is true we will compress the entire track image
        // otherwise we just image that portion of the track specified by the defaultTextBox field
        DisposeRgn(trackRgn);
        trackRgn = GetTrackDisplayBoundsRgn(srcTrack); // rgn could change over time
        trackRect = (*trackRgn)->rgnBBox;
        if (imageTrack) {
            textBox = trackRect;
            OffsetRect(&textBox, -trackRect.left, -trackRect.top);
            (*tdh)->defaultTextBox = textBox;
        } else {
            textBox = (*tdh)->defaultTextBox;
            // fix up textBox so it doesn't extend beyond the track
            if (textBox.right > trackRect.right-trackRect.left)
                textBox.right = trackRect.right-trackRect.left;
            if (textBox.bottom > trackRect.bottom-trackRect.top)
                textBox.bottom = trackRect.bottom-trackRect.top;
            (*tdh)->defaultTextBox = textBox;
        }
 
        // Non-sync samples are hilite samples; we don't need to convert them
        if (sampleFlags & mediaSampleNotSync) {
            addFlags = mediaSampleNotSync;
            goto skipConversion;
        }
 
        // Force update
        MoviesTask(srcMovie, 0);
        MoviesTask(srcMovie, 0);
        
        // Copy bits to window (so the user sees something happening)
        CopyBits((BitMap*)&movieGW->portPixMap, (BitMap*)&movieWindow->portBits, &movieBox, &movieBox, srcCopy, 0);
        
        // Get text metrics data structure for current time (used by text media handler for hiliting)
        // This will be added to the text sample below
        FailOSErr( GetMetrics(textMediaHandler, metrics));
 
        // Use track matrix to find text box's location within the movie box
        TransformRect(&trackMatrix, &textBox, 0);
        // Safety code: Force text box to be within the bounds of the track
        textBox.right = min(textBox.right, trackRect.right);
        textBox.bottom = min(textBox.bottom, trackRect.bottom);
        // More safety code: Don't allow text box height to be too small 
        if ((textBox.bottom - textBox.top) <= 4)
            textBox.bottom = trackRect.bottom;
        
        // Compress the text image
        GetGWorld(&saveGW, &saveGD);
        SetGWorld(movieGW, movieGD);
        ForeColor(blackColor);
        BackColor(whiteColor);
        FailOSErr( FCompressImage(movieGW->portPixMap, &textBox, spatial->depth, spatial->spatialQuality, spatial->codecType, 0, 0, 0, 0, 0, 0, idh, imageData));
        SetGWorld(saveGW, saveGD);
                
        /********************************************************************/
        /*  Build the new text descriptor                                   */
        /********************************************************************/
        
        // If defaultFontName isn't there then add one
        if ((long)&(*tdh)->defaultFontName - (long)(*tdh) >= (*tdh)->size) {
            short fontNumber = (*tdh)->defaultStyle.scrpFont;
            Str255 fontName;
            fontName[0] = 0;
 
            // fontNum of 0 or 1 is special case (sys or app font) -> Don't use name
            if (fontNumber != 0 && fontNumber != 1)
                GetFontName(fontNumber, fontName);
                
            FailMemErr( SetHandleSize((Handle)tdh, sizeof(TextDescription) + fontName[0]));
            BlockMove(fontName, (*tdh)->defaultFontName, fontName[0]+1);
            
            // Don't use sizeof(TextDescription) to set size, since it pads the length to an even byte
            (*tdh)->size = ((long)&(*tdh)->defaultFontName - (long)(*tdh) + 1) + fontName[0];
        }
 
        // increase text description handle size to account for size of the image descriptor + atom header bytes (8)
        FailMemErr( SetHandleSize((Handle)tdh, (*tdh)->size + (*idh)->idSize + 8));
        (*tdh)->size = (*tdh)->size + (*idh)->idSize + 8;
 
        HLock((Handle)tdh);
 
        // Point past the default font name
        descPtr = (Ptr)((long)&(*tdh)->defaultFontName + (*tdh)->defaultFontName[0] + 1);
 
        // image descriptor atom header
        *(long*)descPtr = (*idh)->idSize + 8;
        descPtr += 4;
        *(long*)descPtr = 'idsc';
        descPtr += 4;
 
        // copy image descriptor
        compressedDataSize = (*idh)->dataSize;
        (*idh)->dataSize = 0; // set to zero so that all image descs will be same (field is not used at decompress)
        BlockMove(*idh, descPtr, (*idh)->idSize);
        
        HUnlock((Handle)tdh);
 
        /********************************************************************/
        /*  Build the new text sample                                       */
        /********************************************************************/
        
        // Look for and delete any existing bitmap or metric data from existing sample
        textLen = *((short*) *dataOut);
        if (dataSize > (textLen + 2)) {
            // We got extra stuff
            Ptr     dataPtr = (*dataOut) + textLen + 2;
            long    dataLen;
            
            do  {
                dataLen = *((long*) dataPtr);
                if (dataLen <= 0 || dataLen > dataSize) break; // safety check
                
                if (((*((long*) (dataPtr+4))) == 'imag') || ((*((long*) (dataPtr+4))) == 'metr')) {
                    // Delete the atom by sliding the remaining data over to fill in the gap
                    long restOfDataSize = dataSize - (dataPtr+dataLen - *dataOut);
                    if (restOfDataSize > 0)
                        BlockMove(dataPtr+dataLen, dataPtr, restOfDataSize);
                    dataSize -= dataLen;
                } else
                    dataPtr += dataLen;
            } while ((dataPtr - *dataOut) < dataSize);
        }
    
        // Add Compressed Image to SampleData
        newDataSize = dataSize + compressedDataSize + 16 + GetHandleSize(metrics); // 16 = size of 2 atom headers
        
        FailMemErr( SetHandleSize(dataOut, newDataSize));
 
        HLock(dataOut);
        dataPtr = *dataOut+dataSize;
        
        // 'imag' atom header
        *(long*)dataPtr = compressedDataSize + 16;
        dataPtr += 4;
        *(long*)dataPtr = 'imag';
        dataPtr += 4;
        
        // image data atom header
        *(long*)dataPtr = compressedDataSize + 8;
        dataPtr += 4;
        *(long*)dataPtr = 'idat';
        dataPtr += 4;
        
        // copy image data
        BlockMove(imageData, dataPtr, compressedDataSize);
        
        // copy text metrics
        BlockMove(*metrics, dataPtr+compressedDataSize, GetHandleSize(metrics));
        
        HUnlock(dataOut);
 
skipConversion:
        // Turn off the don't hilite flag
        (*tdh)->displayFlags &= ~dfDontHilite;
        (*tdh)->displayFlags &= ~dfDontBitmap;
        
        // Add the new sample data to new media
        FailOSErr( AddMediaSample(newTextMedia, dataOut, 0, newDataSize, sampleDuration,
                (SampleDescriptionHandle) tdh, 1, addFlags, &sampleTime));
        
        // Insert media into same location (thisTrackTime) in new track
        if (interestingDuration < 0 || interestingDuration > sampleDuration) interestingDuration = sampleDuration;
        dur = GetTrackDuration(dstTrack);
        FailOSErr( InsertMediaIntoTrack(dstTrack, thisTrackTime, sampleTime, interestingDuration, kFix1));
        dur = GetTrackDuration(dstTrack);
            
        // Get time for next text sample
        GetTrackNextInterestingTime(srcTrack, nextTimeMediaSample, thisTrackTime, kFix1, &nextTrackTime, &interestingDuration);
        if (thisTrackTime == nextTrackTime) {
            //DebugStr((StringPtr)"\pGetTrackNextInterestingTime returned same time");
            goto bail;
        }
        thisTrackTime = nextTrackTime;
    }
    err = EndMediaEdits(newTextMedia);
    
    
bail:
    DisposeHandle(dataOut);
    DisposeHandle((Handle)idh);
    DisposeHandle((Handle)tdh);
    DisposeHandle(metrics);
    if (imageData) DisposePtr(imageData);
    if (trackRgn) DisposeRgn(trackRgn);
    
    return err;
    
}