. 포맷변환

다양한 포맷을 지원하려면 외부로부터 데이터를 전달 받을 때 이 데이터가 과연 내부 포맷과 일치하는지 점검해보고 일치하지 않는다면 자신이 관리할 수 있는 포맷으로 변환해야 한다. 외부로부터 데이터를 전달받는 시점은 SetText 함수가 호출될 때, 즉 호스트가 파일을 읽어서 그 내용을 컨트롤에게 줄 때이다. 또한 클립보드를 통해 다른 프로그램이 복사해놓은 텍스트를 가져올 때도 포맷을 점검해야 한다. 다음 함수는 인수로 주어진 Text 문자열의 포맷을 판별한다.

 

DWORD CApiEdit::AnalyzeFormat(TCHAR *Text, int dwSize)

{

     int result;

     TCHAR *p;

 

     result=IS_TEXT_UNICODE_UNICODE_MASK | IS_TEXT_UNICODE_REVERSE_MASK;

     if (IsTextUnicode(Text,lstrlen(Text),&result)) {

          return AE_FORMAT_UNICODE;

     }

 

     if (dwSize != -1) {

          if (lstrlen(Text) != dwSize) {

              return AE_FORMAT_BINARY;

          }

     }

 

     p=strchr(Text,’\r’);

     if (p==NULL) {

          if (strchr(Text,’\n’)) {

              return AE_FORMAT_UNIX;

          } else {

              return AE_FORMAT_WIN;

          }

     } else {

          if (*(p+1)==‘\n’) {

              return AE_FORMAT_WIN;

          } else {

              return AE_FORMAT_MAC;

          }

     }

}

 

먼저 IsTextUnicode 함수를 호출하여 유니코드 포맷인지 본다. IsTextUnicode 함수는 확률에 근거하여 문자열 포맷을 점검하는데 너무 짧은 문자열이 아닌 한 거의 정확하게 분석한다. 어디까지나 확률에 근거하여 판단하기 때문에 틀릴 수도 있는데 만약 이 함수가 틀린 결과를 리턴한다면 ApiEdit는 틀린 결과대로 동작할 수밖에 없다. 이 함수가 TRUE를 리턴하면 더 이상 다른 포맷을 점검해 볼 필요없이 AE_FORMAT_UNICODE를 리턴하면 된다.

유니코드가 아니라면 이진 포맷인지 본다. Text 버퍼에 들어 있는 문자열의 길이가 버퍼 크기와 다르면 중간에 널 종료문자(0)가 있다는 뜻이므로 이진 포맷으로 판단한다. 유니코드 문서는 두 개의 0으로 문서 끝을 표현하며 중간에 0이 있을 수 있으므로 이진 포맷보다 먼저 점검하였다. 이진 포맷은 일정한 규칙이 없으며 임의의 코드가 무작위로 나타날 수 있는 포맷인데 비해 문자열 포맷은 중간에 널 종료문자가 나타날 수 없다는 특징이 있으므로 이 차이점을 이용했다. 이진 포맷 판별도 어디까지나 확률에 근거하기 때문에 이 판단의 결과가 반드시 정확하다고 할 수는 없다.

파일 포맷의 특징 자체가 애매하기 때문에 틀려도 어쩔 수 없다. 윈도우즈의 파일 시스템은 확장자에 의해 파일의 종류를 구분하지만 이것도 절대적인 기준이 아니기 때문에 원칙적으로 파일의 내용으로 종류를 정확하게 판별하는 것은 불가능하다. 확률적으로 가급적 유사한 포맷을 찾아낼 수 있을 뿐이다. 만약 포맷을 잘못 판별하면 화면에 이상한 문자들이 출력되는데 다음은 메모장으로 BMP파일을 열어 본 것이다.

좀 보기에 안 좋기는 하지만 이것은 메모장이 멍청해서 그런 것이 아니다. 메모장뿐만 아니라 워드나 비주얼 스튜디오도 동일한 증상을 보인다. 다행히 포맷을 잘못 판단했다고 해서 프로그램이 죽거나 하지는 않으므로 걱정할 필요는 없으며 판별된 포맷대로 보여주면 된다.

유니코드도 아니고 이진 포맷도 아니라면 ANSI 포맷 중 하나인데 개행코드를 보고 어떤 운영체제에서 만들어진 파일인가만 판단하면 된다. 운영체제에 따른 포맷은 CR/LF 조합으로 판단할 수 있는데 먼저 CR을 찾아 본다. 만약 CR이 발견되지 않는다면 LF를 찾아보고 LF만 있으면 유닉스 포맷이다. CR이 있으면 바로 뒷자리에 LF도 있는지 확인해 본다. LF가 있다면 윈도우즈 포맷이고 LF가 없으면 CR만으로 개행을 하는 매킨토시 포맷이다.

만약 CR도 없고 LF도 없다면 어떤 포맷으로 판단해야 할까? CR, LF가 하나도 없는 텍스트는 두말할 것도 없이 한 줄짜리 문서(또는 빈 문서)이며 개행코드가 없기 때문에 모든 포맷 조건을 다 만족한다고 할 수 있다. 이때는 임의의 포맷으로 분석해도 되지만 디폴트 포맷인 윈도우즈 포맷으로 판별하는 것이 유리하다. 그래야 변환 과정을 생략할 수 있다.

Text의 포맷을 판별해 본 후 만약 윈도우즈 포맷이 아니라면, ApiEdit가 직접 편집할 수 있는 포맷과 다르다면 포맷변환을 해야 한다. 그래야 ApiEdit가 이 텍스트를 다룰 수 있다. 변환된 텍스트를 다시 저장할 때는 원래 포맷대로 다시 변환해야 한다. 다음 함수는 임의의 포맷을 다른 포맷으로 변환한다.

 

int CApiEdit::ConvertFormat(DWORD from, DWORD to, TCHAR *src, TCHAR *&dest)

{

     int need;

     TCHAR *s,*d;

     TCHAR Find;

 

     if (from==AE_FORMAT_UNICODE) {

          need=WideCharToMultiByte(CP_ACP,0,(LPCWSTR)src+1,-1,NULL,0,0,0);

          dest=(TCHAR *)malloc(need);

          WideCharToMultiByte(CP_ACP,0,(LPCWSTR)src+1,-1,dest,need,0,0);

          return need;

     }

 

     if (to==AE_FORMAT_UNICODE) {

          need=MultiByteToWideChar(CP_ACP,0,src,-1,NULL,0);

          need=need*2+2;

          dest=(TCHAR *)malloc(need);

          dest[0]=(TCHAR)0xFF;

          dest[1]=(TCHAR)0xFE;

          MultiByteToWideChar(CP_ACP,0,src,-1,(LPWSTR)dest+1,need);

          return need;

     }

 

     dest=(TCHAR *)malloc(lstrlen(src)*2);

     s=src;

     d=dest;

 

     if (to==AE_FORMAT_WIN) {

          switch (from) {

          case AE_FORMAT_UNIX:

              Find=‘\n’;

              break;

          case AE_FORMAT_MAC:

              Find=‘\r’;

              break;

          }

          while (*s) {

              if (*s==Find) {

                   *d++=‘\r’;

                   *d++=‘\n’;

                   s++;

              } else {

                   *d++=*s++;

              }

          }

          *d=*s;

     } else {

          switch (to) {

          case AE_FORMAT_UNIX:

              Find=‘\n’;

              break;

          case AE_FORMAT_MAC:

              Find=‘\r’;

              break;

          }

          while (*s) {

              if (*s==‘\r’) {

                   *d++=Find;

                   s++;

                   s++;

              } else {

                   *d++=*s++;

              }

          }

          *d=*s;

     }

     return lstrlen(dest);

}

 

from 포맷을 to 포맷으로 바꾼 결과를 dest 번지에 메모리를 할당하여 저장한다. 변환 후의 길이를 미리 예측할 수 없기 때문에 호출측에서 결과 문자열을 돌려받기 위한 버퍼를 미리 할당할 수 없다. 그래서 이 함수가 메모리를 할당해서 넘겨주고 호출측에서 메모리를 해제하도록 되어 있으며 dest 인수는 포인터의 레퍼런스형이다. 리턴값은 변환 후의 결과 문자열의 길이이다.

이 함수는 임의의 포맷을 윈도우즈 포맷으로 바꾸거나 반대로 윈도우즈 포맷을 임의의 포맷으로만 바꿀 수 있다. 그래서 from, to 둘 중 하나는 반드시 AE_FORMAT_WIN이어야 한다. 즉 유닉스 포맷을 매킨토시 포맷으로 바꾸거나, 유니코드 포맷을 곧바로 유닉스 포맷으로 바꾸는 것은 지원하지 않는다. ApiEdit가 윈도우즈 포맷만 인식하므로 이 포맷과 다른 포맷과의 변환만 제대로 하면 될 뿐 그 이상의 서비스를 할 필요는 없다.

유니코드 포맷의 파일은 선두 2바이트에 바이트 순서를 표시하는 바이트 오더링(Byte Ordering)이라는 추가 정보를 가지고 있다. 이 정보는 16비트의 값을 메모리에 어떤 순서로 저장할 것인가를 지정하는데 잘 알다시피 x86계열의 CPU는 역워드로 저장한다. 인텔 계열의 CPU 0xFEFF, 모토로라 계열의 CPU 0xFFFE로 되어 있는데 당근은 인텔 계열에서만 돌아가므로 0xFEFF를 달아 주었다.

운영체제간의 포맷변환은 CR/LF의 조합만 바꾸면 된다. 예를 들어 유닉스 포맷을 윈도우즈 포맷으로 바꾼다면 모든 CR CR/LF로 바꾸면 된다. 단순한 바이트 치환 동작만으로 포맷간의 변환을 할 수 있다. ConvertFomat 함수의 나머지 코드는 앞장에서 작성한 편집 함수들과 비슷하므로 쉽게 분석될 것이다.

포맷 판별 함수와 변환 함수가 완성되었으므로 이제 ApiEdit는 적어도 이 두 함수가 지원하는 포맷은 다 읽을 수 있다. 외부로부터 데이터를 받아들이는 SetText 함수를 다음과 같이 수정한다. 이진 파일인지 판단하기 위해서는 버퍼의 길이가 필요하므로 원형에 인수가 하나 더 추가되었다.

 

void CApiEdit::SetText(TCHAR *TextBuf, int dwSize)

{

     TCHAR *dest;

 

     InitDoc();

     dwFormat=AnalyzeFormat(TextBuf, dwSize);

     if (dwFormat != AE_FORMAT_WIN && dwFormat != AE_FORMAT_BINARY) {

          ConvertFormat(dwFormat,AE_FORMAT_WIN,TextBuf,dest);

          Insert(0,dest,FALSE);

          free(dest);

     } else {

          Insert(0,TextBuf,FALSE);

     }

}

 

전달된 텍스트의 포맷을 AnalyzeFormat 함수로 분석해보고 윈도우즈 포맷이나 이진 포맷이 아니라면 ConvertFormat 함수를 호출하여 윈도우즈 포맷으로 먼저 바꾼다. 변환된 문자열을 Insert 함수로 삽입하면 ApiEdit가 변환된 포맷을 편집할 수 있을 것이다. 윈도우즈 포맷이라면 변환없이 바로 Insert한다. SetText 함수의 인수가 늘어났으므로 호스트에서 이 함수를 호출하는 부분도 수정되어야 한다.

 

BOOL OpenFileToChild(HWND hChild, TCHAR *Path)

{

     ....

    TextBuf=(TCHAR *)malloc(dwSize+2);

     ReadFile(hFile,TextBuf,dwSize,&dwRead,NULL);

     TextBuf[dwRead]=0;

    TextBuf[dwRead+1]=0;

    pSi->Ae.SetText(TextBuf,dwSize);  

     CloseHandle(hFile);

     ....

}

 

유니코드는 널 종료문자의 길이도 16비트이므로 포맷에 상관없이 널 종료문자를 2바이트로 작성해야 한다. 그렇지 않으면 유니코드 문서의 끝을 찾지 못할 것이다. SetText 함수가 버퍼의 크기를 인수로 요구하므로 파일의 크기인 dwSize를 인수로 전달해야 한다. 이제 실행해보면 포맷에 상관없이 문서를 읽을 수 있을 것이다.