home *** CD-ROM | disk | FTP | other *** search
/ OS/2 Shareware BBS: 10 Tools / 10-Tools.zip / mavcl130.zip / MPSETUP.CPP < prev    next >
Text File  |  1996-05-07  |  17KB  |  633 lines

  1. /*  File: MPSETUP.CPP      Updated: Tue Aug 15 15:54:49 1995
  2. Copyright (c) Fabrizio Aversa
  3. ===========================================================*/
  4. // setup.c: printer setup routine
  5. // Monte Copeland for DevCon 7
  6.  
  7. #define INCL_DOSNLS        /* National Language Support values */
  8. #define INCL_DEV
  9. #define INCL_PM
  10. #define INCL_SPLDOSPRINT
  11. #include <os2.h>
  12.  
  13. #include <stdio.h>
  14. #include <stdlib.h>
  15. #include <string.h>
  16.  
  17. #include <iframe.hpp>
  18. #include <icmdhdr.hpp>
  19. #include <istattxt.hpp>
  20. #include <istring.hpp>
  21. #include <imsgbox.hpp>
  22. #include <ilistbox.hpp>
  23. #include <icheckbx.hpp>
  24. #include <icmdhdr.hpp>
  25. #include <iedithdr.hpp>
  26. #include <icheckbx.hpp>
  27. #include <ientryfd.hpp>
  28. #include <iselhdr.hpp>
  29. #include <idate.hpp>
  30.  
  31. #include "mavprint.hpp"
  32. #include "mavconv.hpp"
  33.  
  34. class PrinterDialog : public IFrameWindow,
  35. public ICommandHandler,
  36. public ISelectHandler
  37. {
  38.    public:
  39.  
  40.    PrinterDialog(IWindow *, PPRINTERSETUP, LONG);
  41.    ~PrinterDialog();
  42.  
  43.    private:
  44.  
  45.    PRINTERSETUP         backup;
  46.    PPRINTERSETUP       pSetup, pTarget;
  47.  
  48.    void fillFields();
  49.    virtual Boolean command(ICommandEvent&);
  50.    virtual Boolean selected(IControlEvent& );
  51.  
  52.    ICheckBox * check;
  53.    IEntryField * entry;
  54.    IListBox * list;
  55.  
  56. };
  57.  
  58. PrinterDialog :: PrinterDialog(IWindow* owner, PPRINTERSETUP p1, LONG lngDlgId)
  59. : IFrameWindow(IResourceId(lngDlgId,IDynamicLinkLibrary("mavcl.dll")), owner)
  60. {
  61.    ICommandHandler::handleEventsFor(this);
  62.    ISelectHandler::handleEventsFor(this);
  63.  
  64.    memcpy( &backup, p1, sizeof( PRINTERSETUP ) );
  65.    pSetup =  &backup; // work on backup copy
  66.    pTarget = p1;
  67.  
  68.    check= new ICheckBox(IDC_TOFILE, this);
  69.    entry= new IEntryField(IDC_ENTRY, this);
  70.    list= new IListBox(IDC_LISTBOX, this);
  71.  
  72.    fillFields();
  73. }
  74.  
  75. PrinterDialog :: ~PrinterDialog()
  76. {
  77.    delete check;
  78.    delete entry;
  79.    delete list;
  80. }
  81.  
  82. void PrinterDialog :: fillFields()
  83. {
  84.    LONG j, i, selected = 0;
  85.    char * psz;
  86.  
  87.    for( i = 0; i < pSetup->cQueues; i++ )
  88.    {
  89.       // Display printer comment if possible, else use queue name for display.
  90.       psz = (*pSetup->pQueueInfo[i].pszComment ?
  91.       pSetup->pQueueInfo[i].pszComment : pSetup->pQueueInfo[i].pszName);
  92.  
  93.       /* subst cr and lf with space */
  94.       for (j = 0 ; j < strlen(psz) ; j++)
  95.       if(psz[j] == 13 || psz[j] == 10) psz[j] = ' ';
  96.  
  97.       list->addAsLast( psz );
  98.  
  99.       if( 0 == strcmp( pSetup->pQueueInfo[i].pszName, pSetup->szPreferredQueue )) {
  100.          selected = i;
  101.       }
  102.    }
  103.  
  104.    // Ensure that one queue is selected.
  105.    if( list->count() ) list->select( selected );
  106.  
  107.    // check print-to-file button
  108.    if(! pSetup->fToFile )
  109.    {
  110.       check->deselect();
  111.       entry->disable( );
  112.    } else
  113.    check->select();
  114.  
  115.    // entry field is file name
  116.    if( 0 == strlen( pSetup->szFileName ))
  117.    {
  118.       // put in a default
  119.       strcpy( pSetup->szFileName, "PRINTER.OUT" );
  120.    }
  121.    entry->setText( pSetup->szFileName  );
  122.  
  123. }
  124.  
  125. Boolean PrinterDialog :: command(ICommandEvent& cmdevt)
  126. {
  127.    CHAR                szDeviceName[ 48 ];
  128.    CHAR                szDriverName[ 64 ];
  129.    PPRQINFO3           pqi;
  130.  
  131.    pqi = &pSetup->pQueueInfo[ list->selection() ];
  132.    char * pch;
  133.  
  134.    switch(cmdevt.commandId()) {
  135.  
  136.       case DID_OK:
  137.  
  138.       // New preferred queue. Modify the one in the PRINTERSETUP structure.
  139.       strcpy( pSetup->szPreferredQueue, pqi->pszName );
  140.  
  141.       // query filename
  142.       if( check->isSelected() ) {
  143.          pSetup->fToFile = TRUE;
  144.          strcpy(pSetup->szFileName, entry->text());
  145.       } else {
  146.          pSetup->fToFile = FALSE;
  147.          *pSetup->szFileName = 0;
  148.       }
  149.  
  150.       memcpy( pTarget, pSetup, sizeof( PRINTERSETUP ) );
  151.  
  152.       dismiss(DID_OK);
  153.  
  154.       break;
  155.  
  156.       case DID_CANCEL:
  157.       dismiss(DID_CANCEL);
  158.       break;
  159.  
  160.       case IDC_JOBPROP:
  161.  
  162.       // Call DevPostDeviceModes() to present the job setup dialog of the printer driver.
  163.       // pqi->pszDriverName is DRIVER.Device format. Separate them.
  164.  
  165.       strcpy( szDriverName, pqi->pszDriverName );
  166.  
  167.       pch = strchr( szDriverName, '.' );
  168.       if( pch ) {
  169.          strcpy( szDeviceName, pch+1 );
  170.          *pch = 0;
  171.       }
  172.       else
  173.       {
  174.          *szDeviceName = 0;
  175.       }
  176.  
  177.  
  178.       // There may be more than one printer on this print queue
  179.       pch = strchr( pqi->pszPrinters, ',' );
  180.       if( pch ) {
  181.          *pch = 0;
  182.       }
  183.  
  184.       // Present the job properties dialog to the user.
  185.       if(
  186.       DevPostDeviceModes( pSetup->hab,
  187.       pqi->pDriverData,
  188.       szDriverName,
  189.       szDeviceName,
  190.       pqi->pszPrinters,
  191.       DPDM_POSTJOBPROP )
  192.       == DPDM_ERROR) {
  193.  
  194.          IMessageBox msgbox(this);
  195.          msgbox.setTitle("Impostazione stampante");
  196.          msgbox.show("DevPostDeviceModes",
  197.          IMessageBox::okButton | IMessageBox::errorIcon | IMessageBox::moveable);
  198.  
  199.       }
  200.  
  201.       break;
  202.  
  203.  
  204.    }/* end switch */
  205.  
  206.    return(false); //Allow Default Processing to occur
  207. }
  208.  
  209. Boolean PrinterDialog::selected(IControlEvent& icEvt)
  210. {
  211.    switch(icEvt.controlId()) {
  212.  
  213.       case IDC_TOFILE:
  214.       if( check->isSelected() ) {
  215.          entry->enable();
  216.       } else {
  217.          entry->disable();
  218.       }
  219.       break;
  220.  
  221.    }
  222.  
  223.    return true;
  224.  
  225. }
  226.  
  227. Boolean MavPrint::setupPrinter(IWindow * owner, int iNumCopies)
  228. {
  229.  
  230.    CHAR psz1[80], psz2[80];
  231.  
  232.    *psz1 = 0;
  233.    *psz2 = 0;
  234.  
  235.    return setupPrinter(psz1, psz2, owner, iNumCopies);
  236.  
  237. }
  238.  
  239. Boolean MavPrint::setupPrinter(
  240. PSZ pszUserQueueName,
  241. PSZ pszUserPrintFileName,
  242. IWindow * owner,
  243. int iNumCopies
  244. )
  245. /* fills the PRINTERSETUP structure with relevant data.
  246. call with empty pszUserQueueName if you want the default queue
  247. call with pszUserQueueName if you want ta specific queue you know in advance, in
  248. this case you can also use pszUserPrintFileName.
  249. call with a valid owner if you want the print queue dialog to be shown.
  250. returns TRUE if ok, otherwise FALSE.
  251. Always call with PSZs allocated for the maximum queue name length, they will
  252. be filled on successfull completion with queue/file names. */
  253. {
  254.    BOOL            fOK;
  255.    CHAR            szDefaultQueue[ 196 ];
  256.    CHAR            szSavedQueue[ 196 ];
  257.    CHAR            szWork[ 196 ];
  258.    PCHAR           pch;
  259.    PPRQINFO3       pqi;
  260.    SIZEL           sizel;
  261.    ULONG           cReturned;
  262.    ULONG           cTotal;
  263.    ULONG           cbNeeded;
  264.    ULONG           ul;
  265.    ULONG           ulrc;
  266.  
  267.    // Caller must set these items before calling.
  268.    if(! pSetup->hab ||
  269.    ! pSetup->lWorldCoordinates  ) return FALSE;
  270.  
  271.    // no good unless I can open a PS
  272.    pSetup->pDevOpenData = NULL;
  273.  
  274.    // Close the info DC's and PS's from any previous call.
  275.    if( pSetup->hpsPrinterInfo )
  276.    {
  277.       GpiAssociate( pSetup->hpsPrinterInfo, (HDC)0 );
  278.       GpiDestroyPS( pSetup->hpsPrinterInfo );
  279.       pSetup->hpsPrinterInfo = (HPS)0;
  280.    }
  281.  
  282.    if( pSetup->hdcPrinterInfo )
  283.    {
  284.       DevCloseDC( pSetup->hdcPrinterInfo );
  285.       pSetup->hdcPrinterInfo = (HDC)0;
  286.    }
  287.  
  288.    if( pSetup->pQueueInfo )
  289.    {
  290.       // Free the array of PRQINFO3 from previous call.
  291.       free( pSetup->pQueueInfo );
  292.       pSetup->pQueueInfo = NULL;
  293.    }
  294.  
  295.    // Query how many queues exist on this computer and the
  296.    // number of bytes needed to hold the array.
  297.    ul = SplEnumQueue( NULL, 3, NULL, 0, &cReturned, &cTotal, &cbNeeded, NULL );
  298.    if( cTotal == 0 )
  299.    {
  300.       // There are no queues on this computer!
  301.       ulrc = FALSE;
  302.       pSetup->cQueues = 0;
  303.       return ulrc;
  304.    }
  305.  
  306.    // Allocate memory to store the newly enumerated queue information.
  307.    pSetup->pQueueInfo = (PRQINFO3*)malloc( cbNeeded ) ;
  308.    if( ! pSetup->pQueueInfo )
  309.    {
  310.       ulrc = FALSE;
  311.       return ulrc;
  312.    }
  313.  
  314.    // Call system again to get the array of PRQINFO3 structures.
  315.    ul = SplEnumQueue( NULL, 3, pSetup->pQueueInfo, cbNeeded, &cReturned, &cTotal, &cbNeeded, NULL );
  316.    if( ul != 0 ||
  317.    cReturned != cTotal ) return FALSE;
  318.    pSetup->cQueues = cReturned;
  319.  
  320.    // Establish a default queue -- might need it.
  321.    // Profiled queue name ends with a semicolon.
  322.    ul = PrfQueryProfileString( HINI_PROFILE, "PM_SPOOLER", "QUEUE", NULL, szDefaultQueue, 196 );
  323.    if( ul > 1 ) {
  324.       // Trim off semicolon.
  325.       pch = strchr( szDefaultQueue, ';' );
  326.       *pch = 0;
  327.    } else {
  328.       // Hmmmm. Use the first one queue from the enumeration.
  329.       strcpy( szDefaultQueue,  pSetup->pQueueInfo->pszName );
  330.    }
  331.    if(! strlen( szDefaultQueue ) ) return FALSE;
  332.  
  333.    if( 0 == strlen( pSetup->szPreferredQueue ))
  334.    {
  335.       // No queue preference; use default.
  336.       strcpy( pSetup->szPreferredQueue, szDefaultQueue );
  337.  
  338.       // Don't expect to see DRIVDATA without queue name.
  339.       // if(! pSetup->pDriverData ) return FALSE;
  340.    }
  341.  
  342.    // There is a chance that the preferred queue has been recently deleted.
  343.    // Ensure the preferred queue still exists.
  344.  
  345.    if(*pszUserQueueName) {
  346.  
  347.       pSetup->fToFile = FALSE;
  348.       strcpy( pSetup->szPreferredQueue, pszUserQueueName );
  349.  
  350.       if(*pszUserPrintFileName )
  351.       {
  352.          pSetup->fToFile = TRUE;
  353.          strcpy(pSetup->szFileName, pszUserPrintFileName);
  354.       }
  355.  
  356.    }
  357.  
  358.    pqi = FindQueue( pSetup );
  359.    if( ! pqi )
  360.    {
  361.       // Not found. Use the default queue.
  362.       strcpy( pSetup->szPreferredQueue, szDefaultQueue );
  363.  
  364.       if( pSetup->pDriverData )
  365.       {
  366.          free( pSetup->pDriverData );
  367.          pSetup->pDriverData = NULL;
  368.       }
  369.    }
  370.    else
  371.    {
  372.       // Preferred queue was found in the array. Do some additional validation
  373.       // because it may have changed since last time in this function.
  374.  
  375.       fOK = TRUE;
  376.  
  377.       if( pSetup->pDriverData )
  378.       {
  379.          // Is driver data the right length?
  380.          fOK = fOK && ( pqi->pDriverData->cb == pSetup->pDriverData->cb );
  381.  
  382.          // Is this queue still driving the same device?
  383.          fOK = fOK && ( 0 == strcmp( pqi->pDriverData->szDeviceName,  pSetup->pDriverData->szDeviceName ));
  384.       }
  385.  
  386.       if( !fOK )
  387.       {
  388.          free( pSetup->pDriverData );
  389.          pSetup->pDriverData = NULL;
  390.       }
  391.    }
  392.  
  393.    // Find the queue again. If the last find failed, preferred queue name
  394.    // was changed to default. This find will absolutely always succeed.
  395.  
  396.    pqi = FindQueue( pSetup );
  397.  
  398.    if( !pSetup->pDriverData )
  399.    {
  400.       // Use driver data from the enumeration.
  401.       pSetup->pDriverData = (DRIVDATA*)malloc( pqi->pDriverData->cb );
  402.       if( ! pSetup->pDriverData )
  403.       {
  404.          ulrc = FALSE;
  405.          return ulrc;
  406.       }
  407.       memcpy( pSetup->pDriverData, pqi->pDriverData, pqi->pDriverData->cb );
  408.    }
  409.  
  410.    if(
  411.    ! pSetup->pDriverData ||
  412.    pSetup->pDriverData->cb <= 0 ||
  413.    pSetup->pDriverData->cb != pqi->pDriverData->cb ||
  414.    strcmp( pqi->pDriverData->szDeviceName,  pSetup->pDriverData->szDeviceName )
  415.    ) return FALSE;
  416.  
  417.    /* Note that DriverData used on DevPostDeviceModes() called
  418.    from QueryPrintDlgProc() is the one in the enumerated array,
  419.    not the one in pSetup. This way, QueryPrintDlgProc()
  420.    can massage any/all of the queues in the array before dismissing the dialog.
  421.    */
  422.    memcpy( pqi->pDriverData, pSetup->pDriverData, pSetup->pDriverData->cb );
  423.  
  424.    // Save the name of the preferred queue because the dialogs can change it.
  425.    strcpy( szSavedQueue, pSetup->szPreferredQueue );
  426.  
  427.    if( owner )
  428.    {
  429.  
  430.       /* see if user eats spaghetti */
  431.       COUNTRYINFO  CountryInfo; /* Buffer for country-specific information */
  432.       getCountryInfo(&CountryInfo);
  433.       LONG lngDlgId = (CountryInfo.country == 39 ? ID_PICKQ_IT : ID_PICKQ);
  434.  
  435.       PrinterDialog * printerDialog= new PrinterDialog(owner, pSetup, lngDlgId);
  436.       printerDialog->showModally();
  437.       Boolean booRet= (printerDialog->result() == DID_CANCEL ?  0 : 1);
  438.       delete printerDialog;
  439.       //      if( !booRet ) return TRUE;
  440.  
  441.    } else {
  442.  
  443.       /* see if user passed a preferred queue/file name, in this case use it */
  444.       if(*pszUserQueueName) {
  445.  
  446.          pSetup->fToFile = FALSE;
  447.          strcpy( pSetup->szPreferredQueue, pszUserQueueName );
  448.  
  449.          if(*pszUserPrintFileName )
  450.          {
  451.             pSetup->fToFile = TRUE;
  452.             strcpy(pSetup->szFileName, pszUserPrintFileName);
  453.          }
  454.  
  455.       }
  456.    }
  457.  
  458.    /* return to the user the selected queue/file (for now blank) */
  459.    strcpy(pszUserQueueName,  "" );
  460.    strcpy(pszUserPrintFileName,  "" );
  461.  
  462.    // QueryPrintDlgProc() may have modified pSetup->szPreferredQueue.
  463.    pqi = FindQueue( pSetup );
  464.    if ( ! pqi ) return FALSE;
  465.  
  466.    if( 0 != strcmp( szSavedQueue, pSetup->szPreferredQueue ))
  467.    {
  468.       // The user picked a different queue during dialog processing.
  469.       if(! pSetup->pDriverData ) return FALSE;
  470.       free( pSetup->pDriverData );
  471.  
  472.       pSetup->pDriverData = (DRIVDATA*)malloc( pqi->pDriverData->cb );
  473.       if( ! pSetup->pDriverData )
  474.       {
  475.          ulrc = FALSE;
  476.          return ulrc;
  477.       }
  478.       pSetup->pDriverData->cb = pqi->pDriverData->cb;
  479.    }
  480.  
  481.    // Copy data from the array back to PRINTERSETUP structure.
  482.    if(! pSetup->pDriverData ||
  483.    ! pSetup->pDriverData->cb == pqi->pDriverData->cb ) return FALSE;
  484.    memcpy( pSetup->pDriverData, pqi->pDriverData, pqi->pDriverData->cb );
  485.  
  486.    /* Prepare a DEVOPENSTRUC for DevOpenDC(). Use it here to open an OD_INFO
  487.    DC. Caller may use the same DEVOPENSTRUC to open an OD_QUEUED DC when it is
  488.    time to print. There are 9 pointers in the DEVOPENSTRUC. This code
  489.    prepares the first 4. The others should be NULL.
  490.    */
  491.  
  492.    // Prepare logical address which is preferred queue name.
  493.    if( pSetup->fToFile )
  494.    {
  495.       pSetup->lDCType = OD_DIRECT;
  496.       pSetup->devopenstruc.pszLogAddress = pSetup->szFileName;
  497.    }
  498.    else
  499.    {
  500.       pSetup->lDCType = OD_QUEUED;
  501.       pSetup->devopenstruc.pszLogAddress = pSetup->szPreferredQueue;
  502.    }
  503.  
  504.    // Prepare .DRV file name. Truncate after the period.
  505.    strcpy( szWork, pqi->pszDriverName );
  506.    pch = strchr( szWork, '.' );
  507.    if( pch ) {
  508.       *pch = 0;
  509.    }
  510.    if( pSetup->devopenstruc.pszDriverName )
  511.    {
  512.       free( pSetup->devopenstruc.pszDriverName );
  513.    }
  514.    pSetup->devopenstruc.pszDriverName = (PSZ)malloc( 1 + strlen( szWork ));
  515.    if( ! pSetup->devopenstruc.pszDriverName )
  516.    {
  517.       ulrc = FALSE;
  518.       return ulrc;
  519.    }
  520.    strcpy( pSetup->devopenstruc.pszDriverName, szWork );
  521.  
  522.    // Prepare pointer to driver data.
  523.    pSetup->devopenstruc.pdriv = pSetup->pDriverData;
  524.  
  525.    // Prepare data type. Standard is the preferred way to go.
  526.    pSetup->devopenstruc.pszDataType = "PM_Q_STD";
  527.  
  528.    if(iNumCopies > 1) {
  529.       sprintf(pszQueueProcParams, "COP=%03d", iNumCopies);
  530.       pSetup->devopenstruc.pszQueueProcParams = pszQueueProcParams;
  531.    }
  532.  
  533.    // Open an OD_INFO DC.
  534.    pSetup->hdcPrinterInfo = DevOpenDC( pSetup->hab,
  535.    OD_INFO,
  536.    "*",
  537.    4,
  538.    (PDEVOPENDATA)&pSetup->devopenstruc,
  539.    (HDC)0 );
  540.    if( !pSetup->hdcPrinterInfo ) {
  541.       // Unable to open info DC. WinGetLastError() can provide diagnostics.
  542.       ulrc = FALSE;
  543.       return ulrc;
  544.    }
  545.  
  546.    // Create PS associated with OD_INFO DC.
  547.    sizel.cx = 0;
  548.    sizel.cy = 0;
  549.    pSetup->hpsPrinterInfo = GpiCreatePS( pSetup->hab,
  550.    pSetup->hdcPrinterInfo,
  551.    &sizel,
  552.    pSetup->lWorldCoordinates | GPIA_ASSOC );
  553.  
  554.    if( GPI_ERROR ==  pSetup->hpsPrinterInfo ) {
  555.       // Problem with this setup.
  556.       DevCloseDC( pSetup->hdcPrinterInfo );
  557.       pSetup->hdcPrinterInfo = (HDC)0;
  558.       pSetup->hpsPrinterInfo = (HPS)0;
  559.       ulrc = FALSE;
  560.       return ulrc;
  561.    }
  562.  
  563.    // OK to use.
  564.    pSetup->pDevOpenData = (PDEVOPENDATA)&pSetup->devopenstruc;
  565.  
  566.    /* return to the user the selected queue */
  567.    strcpy(pszUserQueueName, pSetup->szPreferredQueue);
  568.    if(pSetup->fToFile) strcpy(pszUserPrintFileName, pSetup->szFileName);
  569.  
  570.    // Success.
  571.    ulrc = TRUE;
  572.    return ulrc;
  573. }
  574.  
  575. // ---------------------------------------------------------------------------------------------------------
  576. // FindQueue finds the preferred queue name in the PRQINFO3 array.
  577. // Returns the index if found; returns -1 if not found.
  578.  
  579. PPRQINFO3 MavPrint::FindQueue( PPRINTERSETUP pSetup )
  580. {
  581.    LONG   i;
  582.  
  583.    for( i = 0; i < pSetup->cQueues; i++ )
  584.    {
  585.       if( 0 == strcmp( pSetup->szPreferredQueue, pSetup->pQueueInfo[ i ].pszName ) )
  586.       {
  587.          return &pSetup->pQueueInfo[ i ];
  588.       }
  589.    }
  590.    return NULL;
  591. }
  592.  
  593. void MavPrint::cleanupPrinter()
  594. {
  595.  
  596.    // Close DC's and PS's.
  597.  
  598.    if( pSetup->hpsPrinterInfo )
  599.    {
  600.       GpiAssociate( pSetup->hpsPrinterInfo, (HDC)0 );
  601.       GpiDestroyPS( pSetup->hpsPrinterInfo );
  602.       pSetup->hpsPrinterInfo = (HPS)0;
  603.    }
  604.  
  605.    if( pSetup->hdcPrinterInfo )
  606.    {
  607.       DevCloseDC( pSetup->hdcPrinterInfo );
  608.       pSetup->hdcPrinterInfo = (HDC)0;
  609.    }
  610.  
  611.  
  612.    if( pSetup->pQueueInfo )
  613.    {
  614.       free( pSetup->pQueueInfo );
  615.       pSetup->pQueueInfo = NULL;
  616.    }
  617.  
  618.    if( pSetup->pDriverData )
  619.    {
  620.       free( pSetup->pDriverData );
  621.       pSetup->pDriverData = NULL;
  622.    }
  623.  
  624.    if( pSetup->devopenstruc.pszDriverName )
  625.    {
  626.       free( pSetup->devopenstruc.pszDriverName );
  627.       pSetup->devopenstruc.pszDriverName = NULL;
  628.    }
  629.  
  630. }
  631.  
  632.  
  633.