Counter Strike : Global Offensive Source Code
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

669 lines
16 KiB

  1. //===== Copyright � 2005-2006, Valve Corporation, All rights reserved. ======//
  2. //
  3. // Purpose: build a sheet data file and a large image out of multiple images
  4. //
  5. //===========================================================================//
  6. #include "amalg_texture.h"
  7. #include "tier0/platform.h"
  8. #include "tier0/progressbar.h"
  9. #include "mathlib/mathlib.h"
  10. #include "filesystem.h"
  11. #include "tier1/strtools.h"
  12. #include "bitmap/floatbitmap.h"
  13. #include "tier2/fileutils.h"
  14. #include "stdlib.h"
  15. #include "tier0/dbg.h"
  16. static int GetChannelIndexFromChar( char c )
  17. {
  18. // r->0 b->1 g->2 a->3 else -1
  19. static char s_ChannelIDs[]="rgba";
  20. char const *pChanChar = strchr( s_ChannelIDs, c );
  21. if ( ! pChanChar )
  22. {
  23. printf( " bad channel name '%c'\n", c );
  24. return -1;
  25. }
  26. else
  27. {
  28. return pChanChar - s_ChannelIDs;
  29. }
  30. }
  31. static void ZeroChannel( FloatBitMap_t *newBitmap, FloatBitMap_t *pBitmap, int nDestChannel )
  32. {
  33. for ( int y = 0; y < newBitmap->NumRows(); y++ )
  34. {
  35. for ( int x = 0; x < newBitmap->NumCols(); x++ )
  36. {
  37. pBitmap->Pixel( x, y, nDestChannel ) = 0;
  38. }
  39. }
  40. }
  41. static void CopyChannel( FloatBitMap_t *newBitmap, FloatBitMap_t *pBitmap, int nSrcChannel, int nDestChannel )
  42. {
  43. for ( int y = 0; y < newBitmap->NumRows(); y++ )
  44. {
  45. for ( int x = 0; x < newBitmap->NumCols(); x++ )
  46. {
  47. pBitmap->Pixel( x, y, nDestChannel ) = newBitmap->Pixel( x, y, nSrcChannel );
  48. }
  49. }
  50. }
  51. static FloatBitMap_t *CreateFloatBitmap( const char * fname )
  52. {
  53. if ( strchr( fname, ',' ) == NULL )
  54. {
  55. char pFileFullPath[ MAX_PATH ];
  56. if ( !GenerateFullPath( fname, NULL, pFileFullPath, sizeof( pFileFullPath ) ) )
  57. {
  58. Warning( "CDataModel: Unable to generate full path for file %s\n", fname );
  59. return false;
  60. }
  61. return new FloatBitMap_t( pFileFullPath );
  62. }
  63. // parse extended specifications
  64. CUtlVector<char *> Images;
  65. V_SplitString( fname, ",", Images );
  66. FloatBitMap_t *pBitmap = NULL;
  67. // now, process bitmaps, performing copy operations specified by {} syntax
  68. for( int i = 0; i < Images.Count(); i++ )
  69. {
  70. char fnamebuf[MAX_PATH];
  71. strcpy( fnamebuf, Images[i] );
  72. char * pBrace=strchr( fnamebuf, '{' );
  73. if ( pBrace )
  74. {
  75. *pBrace = 0; // null it
  76. pBrace++; // point at control specifier
  77. char *pEndBrace = strchr( pBrace, '}' );
  78. if ( ! pEndBrace )
  79. {
  80. printf( "bad extended bitmap synax (no close brace) - %s \n", Images[i] );
  81. }
  82. }
  83. FloatBitMap_t newBitmap( fnamebuf );
  84. if ( !pBitmap )
  85. {
  86. // first image sets size
  87. pBitmap = new FloatBitMap_t( &newBitmap );
  88. }
  89. // now, process operation specifiers of the form "{chan=chan}" or "{chan=0}"
  90. if ( pBrace && ( pBrace[1] == '=' ) )
  91. {
  92. int nDstChan = GetChannelIndexFromChar( pBrace[0] );
  93. if ( nDstChan != -1 )
  94. {
  95. if ( pBrace[2] == '0' )
  96. {
  97. // zero the channel
  98. ZeroChannel( &newBitmap, pBitmap, nDstChan );
  99. }
  100. else
  101. {
  102. int nSrcChan = GetChannelIndexFromChar( pBrace[2] );
  103. if ( nSrcChan != -1 )
  104. {
  105. // perform the channel copy
  106. CopyChannel( &newBitmap, pBitmap, nSrcChan, nDstChan );
  107. }
  108. }
  109. }
  110. }
  111. }
  112. return pBitmap;
  113. }
  114. CAmalgamatedTexture::CAmalgamatedTexture()
  115. {
  116. m_ePackingMode = PCKM_FLAT;
  117. m_pCurSequence = NULL;
  118. }
  119. void CAmalgamatedTexture::Init( const char *pShtFileName )
  120. {
  121. m_pShtFile = pShtFileName;
  122. }
  123. void CAmalgamatedTexture::SetCurrentSequenceClamp( bool bState )
  124. {
  125. if ( m_pCurSequence )
  126. {
  127. m_pCurSequence->m_Clamp = bState;
  128. }
  129. }
  130. int CAmalgamatedTexture::GetPackingMode()
  131. {
  132. return m_ePackingMode;
  133. }
  134. void CAmalgamatedTexture::SetPackingMode( int eMode )
  135. {
  136. // Assign the packing mode read in to member var.
  137. if ( !m_Sequences.Count() )
  138. {
  139. m_ePackingMode = eMode;
  140. }
  141. else if ( m_ePackingMode != eMode )
  142. {
  143. // Allow special changes:
  144. // flat -> rgb+a
  145. if ( m_ePackingMode == PCKM_FLAT && eMode == PCKM_RGB_A )
  146. {
  147. m_ePackingMode = eMode;
  148. }
  149. // everything else
  150. else
  151. {
  152. printf( "*** line error: incompatible packmode change when %d sequences already defined!\n", m_Sequences.Count() );
  153. //printf( "*** line %d: incompatible packmode change when %d sequences already defined!\n", m_NumActualLinesRead, m_Sequences.Count() );
  154. exit( -1 );
  155. }
  156. }
  157. }
  158. void CAmalgamatedTexture::CreateNewSequence( int sequenceNumber, int mode )
  159. {
  160. m_pCurSequence = new Sequence;
  161. m_pCurSequence->m_nSequenceNumber = sequenceNumber;
  162. SetSequenceType( mode );
  163. m_Sequences.AddToTail( m_pCurSequence );
  164. }
  165. void CAmalgamatedTexture::ValidateSequenceType( int eMode, char *word )
  166. {
  167. switch ( m_ePackingMode )
  168. {
  169. case PCKM_FLAT:
  170. switch ( eMode )
  171. {
  172. case SQM_RGBA:
  173. break;
  174. default:
  175. {
  176. printf( "*** line error: invalid sequence type '%s', packing 'flat' allows only 'sequence-rgba'!\n", word );
  177. //printf( "*** line %d: invalid sequence type '%s', packing 'flat' allows only 'sequence-rgba'!\n", m_NumActualLinesRead, word );
  178. exit( -1 );
  179. }
  180. }
  181. break;
  182. case PCKM_RGB_A:
  183. switch ( eMode )
  184. {
  185. case SQM_RGB:
  186. case SQM_ALPHA:
  187. break;
  188. default:
  189. {
  190. //printf( "*** line %d: invalid sequence type '%s', packing 'rgb+a' allows only 'sequence-rgb' or 'sequence-a'!\n", m_NumActualLinesRead, word );
  191. exit( -1 );
  192. }
  193. }
  194. break;
  195. }
  196. }
  197. int CAmalgamatedTexture::GetSequenceType()
  198. {
  199. return m_pCurSequence->m_eMode;
  200. }
  201. void CAmalgamatedTexture::SetSequenceType( int eMode )
  202. {
  203. m_pCurSequence->m_eMode = eMode;
  204. }
  205. bool CAmalgamatedTexture::CurrentSequenceExists()
  206. {
  207. return m_pCurSequence != NULL;
  208. }
  209. // Validate that frame packing is correct
  210. void CAmalgamatedTexture::ValidateFramePacking( SequenceFrame *pBitmap, char *fileName )
  211. {
  212. if ( m_ePackingMode == PCKM_RGB_A )
  213. {
  214. for ( uint16 idx = 0; idx < pBitmap->m_mapSequences.Count(); ++idx )
  215. {
  216. Sequence *pSeq = pBitmap->m_mapSequences.Key( idx );
  217. if ( pSeq->m_eMode != SQM_RGBA &&
  218. pSeq->m_eMode != m_pCurSequence->m_eMode )
  219. {
  220. printf( "*** line error: 'rgb+a' packing cannot pack frame '%s' belonging to sequences %d and %d!\n",
  221. fileName,
  222. pSeq->m_nSequenceNumber,
  223. m_pCurSequence->m_nSequenceNumber );
  224. //printf( "*** line %d: 'rgb+a' packing cannot pack frame '%s' belonging to sequences %d and %d!\n",
  225. // m_NumActualLinesRead,
  226. // fileName,
  227. // pSeq->m_nSequenceNumber,
  228. // m_pCurSequence->m_nSequenceNumber );
  229. exit( -1 );
  230. }
  231. }
  232. }
  233. }
  234. void CAmalgamatedTexture::CreateFrame( float ftime, CUtlVector<char *> &frameNames )
  235. {
  236. SequenceEntry newSequenceEntry;
  237. newSequenceEntry.m_fDisplayTime = ftime;
  238. for ( int i = 0; i < frameNames.Count(); i++ )
  239. {
  240. LoadFrame( newSequenceEntry, frameNames[i], i );
  241. }
  242. m_pCurSequence->m_Frames.AddToTail( newSequenceEntry );
  243. }
  244. void CAmalgamatedTexture::LoadFrame( SequenceEntry &newSequenceEntry, char *fnamebuf, int frameNumber )
  245. {
  246. SequenceFrame *pBitmap;
  247. // Store the frame in the image list, this is a string - bitmap mapping.
  248. if ( ! ( m_ImageList.Defined( fnamebuf ) ) )
  249. {
  250. SequenceFrame *pNew_frm = new SequenceFrame;
  251. pNew_frm->m_pImage = CreateFloatBitmap( fnamebuf );
  252. pBitmap = pNew_frm;
  253. m_ImageList[ fnamebuf ] = pNew_frm;
  254. }
  255. else
  256. {
  257. pBitmap = m_ImageList[ fnamebuf ];
  258. }
  259. newSequenceEntry.m_pSeqFrame[frameNumber] = pBitmap;
  260. // Validate that frame packing is correct
  261. ValidateFramePacking( pBitmap, fnamebuf );
  262. pBitmap->m_mapSequences.Insert( m_pCurSequence, 1 );
  263. if ( frameNumber == 0 )
  264. {
  265. for( int j = 1; j < MAX_IMAGES_PER_FRAME; j++ )
  266. {
  267. newSequenceEntry.m_pSeqFrame[j] = newSequenceEntry.m_pSeqFrame[0];
  268. }
  269. }
  270. }
  271. void CAmalgamatedTexture::DetermineBestPacking()
  272. {
  273. int nBestWidth = -1;
  274. int nBestSize = (1 << 30 );
  275. int nBestSquareness = ( 1 << 30 ); // how square the texture is
  276. for( int nTryWidth = 2048 ; nTryWidth >= 64; nTryWidth >>= 1 )
  277. {
  278. bool bSuccess = PackImages( NULL, nTryWidth );
  279. if ( bSuccess )
  280. {
  281. printf( "Packing option: %dx%d (%d pixels)\n", m_nWidth, m_nHeight, m_nWidth * m_nHeight );
  282. bool bPreferThisPack = false;
  283. int thisSize = m_nHeight * m_nWidth;
  284. int thisSquareness = ( m_nWidth == m_nHeight ) ? 1 : ( m_nHeight / m_nWidth + m_nWidth / m_nHeight );
  285. if ( thisSize < nBestSize )
  286. {
  287. bPreferThisPack = true;
  288. }
  289. else if ( thisSize == nBestSize && thisSquareness < nBestSquareness )
  290. {
  291. bPreferThisPack = true;
  292. }
  293. if ( bPreferThisPack )
  294. {
  295. nBestWidth = nTryWidth;
  296. nBestSize = thisSize;
  297. nBestSquareness = thisSquareness;
  298. }
  299. }
  300. else
  301. {
  302. break;
  303. }
  304. }
  305. if ( nBestWidth < 0 )
  306. {
  307. printf( "Packing error: failed to pack images!\n" );
  308. exit(1);
  309. }
  310. m_nWidth = nBestWidth;
  311. m_nHeight = nBestSize / nBestWidth;
  312. printf( "Best option: %dx%d (%d pixels)%s\n", m_nWidth, m_nHeight, m_nWidth * m_nHeight, ( m_nWidth == m_nHeight ) ? " : square texture" : "" );
  313. }
  314. bool CAmalgamatedTexture::PackImages( char const *pFilename, int nWidth )
  315. {
  316. switch ( m_ePackingMode )
  317. {
  318. case PCKM_FLAT:
  319. return PackImages_Flat( pFilename, nWidth );
  320. case PCKM_RGB_A:
  321. return PackImages_Rgb_A( pFilename, nWidth );
  322. case PCKM_INVALID:
  323. default:
  324. return false;
  325. }
  326. }
  327. bool CAmalgamatedTexture::PackImages_Flat( char const *pFilename, int nWidth )
  328. {
  329. // !! bug !! packing algorithm is dumb and no error checking is done!
  330. FloatBitMap_t output( nWidth, 2048 );
  331. int cur_line = 0;
  332. int cur_column = 0;
  333. int next_line = 0;
  334. int max_column_written = 0;
  335. for ( int i = 0; i < m_ImageList.GetNumStrings(); i++ )
  336. {
  337. SequenceFrame &frm = *(m_ImageList[i]);
  338. if ( cur_column+frm.m_pImage->NumCols() > output.NumCols() )
  339. {
  340. // no room!
  341. cur_column = 0;
  342. cur_line = next_line;
  343. next_line = cur_line;
  344. }
  345. // now, pack
  346. if ( ( cur_column + frm.m_pImage->NumCols() > output.NumCols() ) ||
  347. ( cur_line + frm.m_pImage->NumRows() > output.NumRows() ) )
  348. {
  349. return false; // didn't fit! doh
  350. }
  351. frm.m_XCoord = cur_column;
  352. frm.m_YCoord = cur_line;
  353. if ( pFilename ) // don't actually pack the pixel if we're not keeping them
  354. {
  355. for ( int y = 0; y < frm.m_pImage->NumRows(); y++ )
  356. for ( int x = 0; x < frm.m_pImage->NumCols(); x++ )
  357. for ( int c = 0; c < 4; c++ )
  358. {
  359. output.Pixel( x+cur_column,y+cur_line, c ) = frm.m_pImage->Pixel(x, y, c);
  360. }
  361. }
  362. next_line = max( next_line, cur_line+frm.m_pImage->NumRows() );
  363. cur_column += frm.m_pImage->NumCols();
  364. max_column_written = max( max_column_written, cur_column );
  365. }
  366. // now, truncate height
  367. int h = 1;
  368. for( h; h < next_line; h *= 2 )
  369. ;
  370. // truncate width;
  371. int w = 1;
  372. for( 1; w < max_column_written; w *= 2 )
  373. ;
  374. if ( pFilename )
  375. {
  376. FloatBitMap_t cropped_output( w, h );
  377. for( int y = 0;y < cropped_output.NumRows(); y++ )
  378. {
  379. for( int x = 0; x < cropped_output.NumCols(); x++ )
  380. {
  381. for( int c = 0; c < 4; c++ )
  382. {
  383. cropped_output.Pixel( x,y,c ) = output.Pixel( x,y,c );
  384. }
  385. }
  386. }
  387. bool bWritten = cropped_output.WriteTGAFile( pFilename );
  388. if ( !bWritten )
  389. printf( "Error: failed to save TGA \"%s\"!\n", pFilename );
  390. else
  391. printf( "Ok: successfully saved TGA \"%s\"\n", pFilename );
  392. }
  393. // Store these for UV calculation later on
  394. m_nHeight = h;
  395. m_nWidth = w;
  396. return true;
  397. }
  398. bool CAmalgamatedTexture::PackImages_Rgb_A( char const *pFilename, int nWidth )
  399. {
  400. // !! bug !! packing algorithm is dumb and no error checking is done!
  401. FloatBitMap_t output( nWidth, 2048 );
  402. int cur_line[2] = {0};
  403. int cur_column[2] = {0};
  404. int next_line[2] = {0};
  405. int max_column_written[2] = {0};
  406. bool bPackingRGBA = true;
  407. for ( int i = 0; i < m_ImageList.GetNumStrings(); i++ )
  408. {
  409. SequenceFrame &frm = *( m_ImageList[i] );
  410. int idxfrm;
  411. int eMode = frm.m_mapSequences.Key( 0 )->m_eMode;
  412. switch ( eMode )
  413. {
  414. case SQM_RGB:
  415. idxfrm = 0;
  416. bPackingRGBA = false;
  417. break;
  418. case SQM_ALPHA:
  419. idxfrm = 1;
  420. bPackingRGBA = false;
  421. break;
  422. case SQM_RGBA:
  423. if ( !bPackingRGBA )
  424. {
  425. printf( "*** error when packing 'rgb+a', bad sequence %d encountered for frame '%s' after all rgba frames packed!\n",
  426. frm.m_mapSequences.Key( 0 )->m_nSequenceNumber,
  427. m_ImageList.String( i ) );
  428. exit( -1 );
  429. }
  430. idxfrm = 0;
  431. break;
  432. default:
  433. {
  434. printf( "*** error when packing 'rgb+a', bad sequence %d encountered for frame '%s'!\n",
  435. frm.m_mapSequences.Key( 0 )->m_nSequenceNumber,
  436. m_ImageList.String( i ) );
  437. exit( -1 );
  438. }
  439. }
  440. if ( cur_column[idxfrm] + frm.m_pImage->NumCols() > output.NumCols() )
  441. {
  442. // no room!
  443. cur_column[idxfrm] = 0;
  444. cur_line[idxfrm] = next_line[idxfrm];
  445. next_line[idxfrm] = cur_line[idxfrm];
  446. }
  447. // now, pack
  448. if ( ( cur_column[idxfrm] + frm.m_pImage->NumCols() > output.NumCols() ) ||
  449. ( cur_line[idxfrm] + frm.m_pImage->NumRows() > output.NumRows() ) )
  450. {
  451. return false; // didn't fit! doh
  452. }
  453. frm.m_XCoord = cur_column[idxfrm];
  454. frm.m_YCoord = cur_line[idxfrm];
  455. if ( pFilename ) // don't actually pack the pixel if we're not keeping them
  456. {
  457. for ( int y = 0; y < frm.m_pImage->NumRows(); y++ )
  458. {
  459. for (int x = 0; x < frm.m_pImage->NumCols(); x++ )
  460. {
  461. for(int c = 0; c < 4; c ++ )
  462. {
  463. switch ( eMode )
  464. {
  465. case SQM_RGB:
  466. if ( c < 3 )
  467. goto setpx;
  468. break;
  469. case SQM_ALPHA:
  470. if ( c == 3 )
  471. goto setpx;
  472. break;
  473. case SQM_RGBA:
  474. if ( c < 4 )
  475. goto setpx;
  476. break;
  477. setpx:
  478. output.Pixel( x + cur_column[idxfrm], y + cur_line[idxfrm], c ) = frm.m_pImage->Pixel(x, y, c);
  479. }
  480. }
  481. }
  482. }
  483. }
  484. next_line[idxfrm] = max( next_line[idxfrm], cur_line[idxfrm] + frm.m_pImage->NumRows() );
  485. cur_column[idxfrm] += frm.m_pImage->NumCols();
  486. max_column_written[idxfrm] = max( max_column_written[idxfrm], cur_column[idxfrm] );
  487. if ( bPackingRGBA )
  488. {
  489. cur_line[1] = cur_line[0];
  490. cur_column[1] = cur_column[0];
  491. next_line[1] = next_line[0];
  492. max_column_written[1] = max_column_written[0];
  493. }
  494. }
  495. // now, truncate height
  496. int h = 1;
  497. for ( int idxfrm = 0; idxfrm < 2; ++idxfrm )
  498. {
  499. for ( h; h < next_line[idxfrm]; h *= 2 )
  500. continue;
  501. }
  502. // truncate width;
  503. int w = 1;
  504. for ( int idxfrm = 0; idxfrm < 2; ++idxfrm )
  505. {
  506. for ( w; w < max_column_written[idxfrm]; w *= 2 )
  507. continue;
  508. }
  509. if ( pFilename )
  510. {
  511. FloatBitMap_t cropped_output( w, h );
  512. for( int y = 0; y < cropped_output.NumRows(); y++ )
  513. {
  514. for( int x = 0; x < cropped_output.NumCols(); x++ )
  515. {
  516. for( int c = 0; c < 4; c++ )
  517. {
  518. cropped_output.Pixel( x, y, c ) = output.Pixel( x, y, c );
  519. }
  520. }
  521. }
  522. bool bWritten = cropped_output.WriteTGAFile( pFilename );
  523. if ( !bWritten )
  524. {
  525. printf( "Error: failed to save TGA \"%s\"!\n", pFilename );
  526. }
  527. else
  528. {
  529. printf( "Ok: successfully saved TGA \"%s\"\n", pFilename );
  530. }
  531. }
  532. // Store these for UV calculation later on
  533. m_nHeight = h;
  534. m_nWidth = w;
  535. return true;
  536. }
  537. void CAmalgamatedTexture::WriteFile()
  538. {
  539. if ( m_pShtFile == NULL )
  540. {
  541. printf( "Error: No output filename set!\n" );
  542. return;
  543. }
  544. COutputFile Outfile( m_pShtFile );
  545. if ( !Outfile.IsOk() )
  546. {
  547. printf( "Error: failed to write SHT \"%s\"!\n", m_pShtFile );
  548. return;
  549. }
  550. Outfile.PutInt( 1 ); // version #
  551. Outfile.PutInt( m_Sequences.Count() );
  552. for ( int i = 0; i < m_Sequences.Count(); i++ )
  553. {
  554. Outfile.PutInt( m_Sequences[i]->m_nSequenceNumber );
  555. Outfile.PutInt( m_Sequences[i]->m_Clamp );
  556. Outfile.PutInt( m_Sequences[i]->m_Frames.Count() );
  557. // write total sequence length
  558. float fTotal = 0.0;
  559. for ( int j = 0; j < m_Sequences[i]->m_Frames.Count(); j++ )
  560. {
  561. fTotal += m_Sequences[i]->m_Frames[j].m_fDisplayTime;
  562. }
  563. Outfile.PutFloat( fTotal );
  564. for( int j = 0; j < m_Sequences[i]->m_Frames.Count(); j++ )
  565. {
  566. Outfile.PutFloat( m_Sequences[i]->m_Frames[j].m_fDisplayTime );
  567. // output texture coordinates
  568. for( int t = 0; t < MAX_IMAGES_PER_FRAME; t++ )
  569. {
  570. //xmin
  571. Outfile.PutFloat( UCoord( m_Sequences[i]->m_Frames[j].m_pSeqFrame[t]->m_XCoord ) );
  572. //ymin
  573. Outfile.PutFloat( VCoord( m_Sequences[i]->m_Frames[j].m_pSeqFrame[t]->m_YCoord ) );
  574. //xmax
  575. Outfile.PutFloat(
  576. UCoord( m_Sequences[i]->m_Frames[j].m_pSeqFrame[t]->m_XCoord +
  577. m_Sequences[i]->m_Frames[j].m_pSeqFrame[t]->m_pImage->NumCols() - 1 ));
  578. //ymax
  579. Outfile.PutFloat(
  580. VCoord( m_Sequences[i]->m_Frames[j].m_pSeqFrame[t]->m_YCoord +
  581. m_Sequences[i]->m_Frames[j].m_pSeqFrame[t]->m_pImage->NumRows() - 1 ));
  582. // printf( "T %d UV1:( %.2f, %.2f ) UV2:( %.2f, %.2f )\n", t,
  583. // UCoord( Sequences[i]->m_Frames[j].m_pSeqFrame[t]->m_XCoord ),
  584. // VCoord( Sequences[i]->m_Frames[j].m_pSeqFrame[t]->m_YCoord ),
  585. // UCoord( Sequences[i]->m_Frames[j].m_pSeqFrame[t]->m_XCoord+Sequences[i]->m_Frames[j].m_pSeqFrame[t]->m_pImage->NumCols()-1 ),
  586. // VCoord( Sequences[i]->m_Frames[j].m_pSeqFrame[t]->m_YCoord+Sequences[i]->m_Frames[j].m_pSeqFrame[t]->m_pImage->NumRows()-1 ));
  587. }
  588. }
  589. }
  590. printf( "Ok: successfully saved SHT \"%s\"\n", m_pShtFile );
  591. }