Site icon Wireframe

[c/c++] SMI를 SRT로 컨버팅하기

Handbrake같은 인코딩 도구를 사용하여 동영상을 인코딩할 때 자막을 영상에 포함하는 옵션이 있는데, 이 때 사용할 수 있는 자막 포맷은 .SRT 형식이다. 국내에서 유통되는 대부분의 자막 포맷은 .SMI 파일이라서, 이를 SRT로 컨버팅할 수 있는 도구가 있으면 좋다. 예전에 맥에서 사용하려고 뒤져봤던 코드가 있었는데, (해당 코드는 다음 페이지에 있다.) 하도 오래전이라 조금 보기 편하게 하려고 수정을 했는데…. 생각보다 너무 오래전 스타일로 작성되어 있더라.(원 글이 작성된 11년 전 기준으로도 되게 오래된 코드였음) C로 새로 구현한 버전도 있긴 한데, 이렇게 공개해봤자 사실 쓰려는 사람도 없을 것 같아서, 대신에 파이썬으로 작성한 버전을 올려본다.

import re
import sys
from pathlib import Path

pat_sync = re.compile(r"^.*sync start=\"?(\d+)", re.IGNORECASE)
pat_br = re.compile(r"(?:<br/?>/s*)+")
pat_tag = re.compile(r"<[^>]+>|&\S+;")


def remove_tags(line: str) -> str:
    temp = pat_br.sub("\n", line).splitlines()
    return "\n".join(re.sub(r"\S+", " ", pat_tag.sub("", x)) for x in temp)


def convert(t: int) -> str:
    q1, r = divmod(t, 3600_000)
    q2, r = divmod(r, 60000)
    q3, r = divmod(r, 1000)
    return f"{q1:02d}:{q2:02d}:{q3:02d}.{r:03d}"


def make_output(buf: list[str], line_n: int, start_t: int, end_t: int) -> str:
    temp = remove_tags("\n".join(buf))
    if re.search(r"\S", temp):
        return f"{line_n}\n{convert(start_t)} --> {convert(end_t)}\n{temp}\n"
    return ""


def process(content: str) -> str:
    buf: list[str] = []
    res: list[str] = []
    line_n = 1
    start_t, end_t = 0, 0
    flag = False

    for line in content.splitlines():
        m = pat_sync.search(line)
        match (m, flag):
            case (None, True):
                buf.append(line)
            case (mat, False) if mat is not None:
                start_t = int(mat.group(1))
                buf.append(line)
                flag = True
            case (mat, True) if mat is not None:
                end_t = int(mat.group(1))
                out = make_output(buf, line_n, start_t, end_t)
                if out:
                    res.append(out)
                start_t, line_n = end_t, line_n + 1
                buf[:] = [line]
            case _:
                continue
    return "\n".join(res)


def main():
    if len(sys.argv) > 1:
        fname = Path(sys.argv[1])
        res = process(fname.read_text())
        if res:
            with fname.with_suffix(".srt").open("w") as f:
                f.write(res)
    else:
        print("Usage: smi2srt.py filename.smi")

    if __name__ == "__main__":
        main()
// vi: et sw=2 nu
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define EXTENSION ".srt" // default extension for output
#define BODY "<BODY>"
#define BUFFER 65535
#define SYNCTAG "<SYNC START="

char *change_extension(char *in, char *out);
char *convert_timestamp(long int, char *);
char *copy_without_space(char *, char *);
char *remove_br_tag(char *);
char *remove_tags(char *);
char *wrap_line(char *);
int error(int); // message from table
int is_not_empty(char *);
void check_commandline_arg(char *);

FILE *in_file;
FILE *out_file;
char *buffer;
char *found_sync_tag, *found_new_sync_tag, *alt_buffer;
char *inputline;
char *outputline;
char time_b[32];
char time_e[32];
long int time_begin = 0;
long int time_end = 0;
unsigned int STRING_LENGTH = 70;
unsigned int linenm = 1;
unsigned int printable = 0;

int main(int argc, char *argv[]) {
  int flag = 0, flag2 = 0;
  char *in_filename = (char *)malloc(255);
  char *out_filename = (char *)malloc(255);
  strcpy(out_filename, "CON");
  printf("\nConvert SMI to SRT...\n");

  if (argc == 1) { // no param
    error(0);
    return 0;
  } else {
    strcpy(in_filename, argv[1]);
    if (argc > 2) {
      strcpy(out_filename, argv[2]);
    } else {
      change_extension(in_filename, out_filename);
    }
    if (argc > 3)
      check_commandline_arg(argv[3]);
  }

  in_file = fopen(in_filename, "r");
  out_file = fopen(out_filename, "w");

  if (!(size_t)out_file)
    return error(2);
  if (!(size_t)in_file)
    return error(1);

  if (!(buffer = (char *)malloc(BUFFER)))
    return error(3);
  if (!(inputline = (char *)malloc(BUFFER)) || (size_t)inputline == -1)
    return error(3);
  if (!(outputline = (char *)malloc(BUFFER)) || (size_t)outputline == -1)
    return error(3);
  if (!(alt_buffer = (char *)malloc(BUFFER)) || (size_t)alt_buffer == -1)
    return error(3);

  printf("Converting from %s to %s\n", in_filename, out_filename);

  while (1) {
    // <BODY> 태그를 찾을 때까지 각 행을 읽어서 진행하고
    // 파일에서 못찾으면 종료
    // `fgets`는 최대 버퍼 길이만큼 읽거나, 개행문자를 만날때까지 읽음
    if (!fgets(buffer, BUFFER, in_file))
      return error(3);
    strupr(buffer);
    if (strstr(buffer, BODY))
      break;
  }

  // MAIN LOOP
  flag = 0;
  while (!flag) {
    if (flag2) {
      flag2 = 0;
      strcpy(buffer, alt_buffer);
    } else {
      // 만약 파일의 끝이었다면 종료
      if (!fgets(buffer, BUFFER, in_file)) {
        break;
      }
    }

    *inputline = '\0';
    strcpy(inputline, buffer);
    strupr(buffer);

    found_sync_tag = strstr(buffer, SYNCTAG);
    if (!found_sync_tag)
      continue;

    while (!flag2) {
      if (!fgets(buffer, BUFFER, in_file)) {
        flag = 1;
        break;
      } // EOF

      // 만약 또 SYNC 태그가 발견됐다면 inputline에 읽었던 라인을 추가
      // 즉 새로운 SYNC 태그를 발견하기 전까지 계속 inputline에 새 라인을 추가
      strcpy(alt_buffer, buffer);
      strupr(buffer);
      found_new_sync_tag = strstr(buffer, SYNCTAG);
      if (!found_new_sync_tag)
        strcat(inputline, alt_buffer);
      else
        flag2 = 1; // END OF SUB. LINE FOUND
    }

    //  태그 처리를 위해 1건의 대사 정보가 연결된 inputline을 buffer로 다시
    //  복사.
    strcpy(buffer, inputline);
    strupr(buffer);
    found_new_sync_tag = strstr(buffer, SYNCTAG);
    sscanf(found_new_sync_tag, "<SYNC START=%d", &time_begin);
    if (printable) {
      strcpy(time_e, time_b);
      convert_timestamp(time_begin, time_b);
      time_end = time_begin;
      fprintf(out_file, "%d\n", linenm++);
      fprintf(out_file, "%s --> %s\n", time_e, time_b);
      fprintf(out_file, "%s\n", outputline);
    } else {
      convert_timestamp(time_begin, time_b);
      strcpy(time_e, time_b);
      time_end = time_begin;
    }

    // LINE CONVERTING
    remove_br_tag(inputline);
    remove_tags(inputline);
    copy_without_space(inputline, outputline);
    wrap_line(outputline);
    printable = is_not_empty(outputline);
  }
  fclose(in_file);
  fclose(out_file);
  free(buffer);
  free(inputline);
  free(outputline);
  free(in_filename);
  free(out_filename);
  printf("converted a %d lines \n\n", linenm - 1);
  return 0;
}

int error(int code) {
  const char errors[][80] = {"use: smi2srt.exe filename.smi [filename.srt] "
                             "[-170] More see in readme.txe",
                             "Unable to open input file", "Memory too low",
                             "<body> not found"};
  printf("Error %d (%s)", code, errors[code]);
  return code;
}

char *remove_br_tag(char *line) {
  char *temp = strstr(line, "<br>");
  while (temp) {
    *temp = '\n';
    *(temp + 1) = ' ';
    *(temp + 2) = ' ';
    *(temp + 3) = ' ';
    temp = strstr(temp, "<br>");
  }
  return line;
}

char *remove_tags(char *line) {
  char *temp = line;
  while (*temp) {
    if (*temp == '<') {
      while (temp) {
        if (*temp == '>') {
          *temp = ' ';
          break;
        }
        *(temp++) = ' ';
      }
    }
    if (*temp == '&') {
      while (*temp) {
        if (*(temp + 1) == ' ') {
          temp++;
          break;
        }
        if (*(temp + 2) == ';') {
          *temp = ' ';
          break;
        }
        *(temp++) = ' ';
      }
    } else
      temp++;
  }
  return line;
}

char *copy_without_space(char *in, char *out) {
  // in -> out으로 문자열을 복사하면서, 연속된 공백은 한 개의 공백으로 변경한다.
  int sp = 1;
  while (*in) {
    if (*in == ' ' && sp == 1) {
      in++;
      continue;
    }
    if (*in == ' ' && sp == 0)
      sp = 1;
    else
      sp = 0;
    *(out++) = *(in++);
  }
  *out = '\0';
  return out;
}

char *convert_timestamp(long int number, char *line) {
  // ms값을 hh:mm:ss.zzz의 형식으로 변환한다.
  long int q, r;
  char vals[32][4];
  q = number / 3600000;
  r = number % 3600000;
  sprintf(vals[0], "%02d", q);
  q = r / 60000;
  r = r % 60000;
  sprintf(vals[1], "%02d", q);
  q = r / 1000;
  r = r % 1000;
  sprintf(vals[2], "%2d", q);
  sprintf(vals[3], "%03d", r);
  sprintf(line, "%s:%s:%s.%s", vals[0], vals[1], vals[2], vals[3]);
  return line;
}

char *wrap_line(char *line) {
  // line이 미리 정해진 STRING_LENGTH 보다 크면
  // 그 앞 공백에서 줄바꿈한다.
  // 줄바꿈하면서 연속된 개행문자가 있으면 공백으로 치환한다.
  size_t i = 0;
  int old = 0;
  int flag = 0;
  while (1) {
    old = i;
    i = i + STRING_LENGTH;
    if (strlen(line) <= i)
      break;
    while (i > old) {
      if (*(line + i) == ' ') {
        *(line + i) = '\n';
        break;
      }
      if (*line == '\n')
        break;
      i--;
    }
  }
  // REMOVE DUPLICATED "\n"
  flag = 0;
  if (*line == '\n') {
    flag = 1;
    *line = ' ';
  }
  while (*line) {
    if (*line == '\n') {
      if (flag)
        *line = ' ';
      else
        flag = 1;
    } else if (*line != ' ')
      flag = 0;
    line++;
  }
  return line;
}

int is_not_empty(char *line) {
  // 문자열에 공백만 있는지 판단한다.
  // 공백만 있는 대사라인이라면 출력하지 않는다.
  while (*line) {
    if (*line != ' ' && *line != '\n')
      return 1;
    line++;
  }
  return 0;
}

void check_commandline_arg(char *line) {
  // 명령줄 인자에 -l 옵션이 있는지 검사한다.
  sscanf(line, "-l%d", &STRING_LENGTH);
}

char *change_extension(char *in, char *out) {
  // in -> out 으로 문자열을 복사하면서,
  // 확장자를 .srt로 변경해준다.
  char *temp;
  strcpy(out, in);
  temp = out + strlen(out);
  while (temp != out) {
    if (*temp == '.') {
      *temp = '\0';
      break;
    }
    temp--;
  }
  strcat(out, EXTENSION);
  return out;
}
Exit mobile version