Como extrair apenas o texto de um conteúdo RTF

Qualquer que seja a linguagem de programação, para ela ser considerada boa suficiente precisa ter os comandos necessários de estrutura de loop, condicionantes, variáveis e alguns outros elementos. O mesmo vale para linguagens procedurais SQL(psql daqui em diante).

Talvez um excelente exemplo para provar o valor da linguagem psql do banco de dados FirebirdSQL seja como extrair de um conteúdo RTF apenas o texto. Eu fiz o exemplo abaixo alguns anos atrás, e ele continua muito eficiente.

Qual o objetivo de extrair o conteúdo textual de um RTF? Você deve saber que no Windows, quase sempre texto rico, isto é, texto com negrito/itálico/sublinhado, alinhamentos, bullets e afins são editados e armazenados no formato RichText, também conhecido como RTF. Um problema com este formato é quando precisamos analisar as informações contidas alí dentro os códigos de controle estão concatenados ao texto impedindo uma leitura suave.

O código abaixo fará essa extração, mas ao invés de você apenas “copiar” e “colar” para suas atividades, estude-o, veja a lógica utilizada. Se entendê-lo, poderá adaptar-se a situações diferentes, por exemplo extrair texto de conteúdos HTML.

O psql abaixo está no formato do FirebirdSQL para SQL Functions, SQL Functions são funções que retornam apenas um tipo de informação, no exemplo abaixo ela se chamará GET_UNRTF e tem um parâmetro de entrada que é um blob do tipo texto cujo formato é RTF e retorna o conteúdo deste blob em formato blob textual também, porém plaintext.

O script abaixo foi testado em diversas situações e é plenamente funcional e até o momento livre de bugs. No entanto, os testes foram em cima de arquivos gerados pelo ms-wordpad que como sabemos é bem mais limitado do que um RTF gerado pelo LibreOffice ou MSWord. Segue:

create or alter function GET_UNRTF (
    P_SOURCE_RTF blob sub_type text)
returns blob sub_type text
AS
declare variable TAGSTART integer;
declare variable TAGFINISH integer;
declare variable I integer;
declare variable TAGFOUND blob sub_type text;
declare variable RESULT_TEXT blob sub_type text;
declare variable TAG_OPEN varchar(255) = '';
declare variable TAG_CLOSE varchar(255) = '';
declare variable CHAR_TEST varchar(255);
declare variable CHAR_HEX varchar(255);
declare variable CHAR_HEX_TO_STR varchar(255);
declare variable HEXA_LEN integer = 2;
declare variable C_QUOTE varchar(1);
declare variable C_CRLF varchar(10);
declare variable C_SPACE varchar(10);
declare variable C_TAG_PAR varchar(10);
declare variable C_SLASH varchar(10);
begin
  -- essa procedure/função retorna um texto (blob) sem a porção de tags rtf, ex:
  -- ret=STR_UNRTF(string_rtf);   // resultado: texto sem as tags rtf
  -- Util para tornar um texto RTF texto pesquisável.
  result_text='';
  p_source_rtf=trim(:p_source_rtf);
  c_quote='''';
  c_crlf= ascii_char(13)||ascii_char(10);
  c_space=ascii_char(32);
  c_tag_par='\par';
  c_slash='\';
  tag_open='{';
  tag_close='}';

  -- todo cabecalho de arquivo RTF possui "{\rtf" então é possivel conferir se
  -- o parametro de entrada esta mesmo no formato RTF, caaso não esteja então
  -- basta retornar o mesmo valor que o parametro de entrada. Isso permite
  -- ganho de velocidade absurdo.
  if (position('{\rtf', :p_source_rtf)<=0) then
  begin
    result_text=p_source_rtf;
    return :result_text;
    exit;
  end

  -- RTF é bagunçado com quebra de linhas, existe tanto o CRLF como também a tag \par 
  -- E o pior é que tem \par seguido de CRLF que se convertido individualmente
  --   produziria duas linhas vazias ao inves de uma
  -- Então é preciso substituí-los por CRLF antes de remover todas as tags, e deve
  --   ser com todo cuidado.
  if (position(:c_tag_par, :p_source_rtf)>0) then
  begin
    p_source_rtf=replace (:p_source_rtf, :c_tag_par||c_space||c_crlf, :c_crlf);
    p_source_rtf=replace (:p_source_rtf, :c_tag_par||c_crlf, :c_crlf);
    p_source_rtf=replace (:p_source_rtf, :c_tag_par||c_space, :c_crlf);
    p_source_rtf=replace (:p_source_rtf, :c_tag_par, :c_crlf);
  end
  -- porém o primeiro caractere se começar com {  ou o ultimo caracter terminar com }
  -- deverá ser removido
  char_test=left(:p_source_rtf,char_length(:tag_open));
  if (char_test=:tag_open) then
  begin
    p_source_rtf=substring(:p_source_rtf from 2 for char_length(:p_source_rtf));
    p_source_rtf=trim(:p_source_rtf);
  end
  char_test=right(:p_source_rtf,char_length(:tag_close));
  if (char_test=:tag_close) then
  begin
    p_source_rtf=substring(:p_source_rtf from 1 for char_length(:p_source_rtf)-char_length(:tag_close));
    p_source_rtf=trim(:p_source_rtf);
  end

  -- Remove tudo que estiver em {tag}
  tagstart = position (:tag_open, :p_source_rtf);
  while (:tagstart > 0) do
  begin
    tagfinish = position (:tag_close, :p_source_rtf, :tagstart);
    if (:tagfinish<:tagstart) then
      tagfinish=char_length(:p_source_rtf);
    tagfound = substring (:p_source_rtf from :tagstart for ((:tagfinish - :tagstart) + 1));
    p_source_rtf = replace (:p_source_rtf, :tagfound, '');
    tagstart = position (:tag_open, :p_source_rtf);
  end

  -- RTF tem \escape para caracteres especiais, ex: fabrica\'e7\'e3o = fabricação
  -- È preciso localizar todos os escapes e trocá-los pelas suas referencias Hexa->Ascii
  tag_open=:c_slash||:c_quote;
  tag_close=:c_space;
  tagstart = position (:tag_open, :p_source_rtf);
  char_hex='';
  hexa_len=2+(char_length(:tag_open));
  while (:tagstart > 0) do
  begin
    char_hex=substring(:p_source_rtf from :tagstart for :hexa_len);
    char_hex_to_str='0x'||substring(:char_hex from char_length(:tag_open)+1);
    i=cast(:char_hex_to_str as int);
    if (i>0) then
    begin
      char_hex_to_str=ascii_char(i);
    end
    else
    begin
      char_hex_to_str='{'||:char_hex_to_str||'}';
    end
    p_source_rtf = replace (:p_source_rtf, :char_hex, :char_hex_to_str);
    tagstart = position (:tag_open, :p_source_rtf);
  end
  p_source_rtf=trim(:p_source_rtf);

  -- RTF tem tags assim:
  -- \viewkind4\uc1\pard\sa200\sl276\slmult1\lang1046\fs20 blabla bla bla
  -- É preciso localizar essas tags e trocar por vazios

  tag_open=:c_slash;
  tag_close=:c_space;
  tagstart = position (:tag_open, :p_source_rtf);
  while (:tagstart > 0) do
  begin
    tagfinish = position (:tag_close, :p_source_rtf, :tagstart);
    if (:tagfinish<:tagstart) then
      tagfinish=char_length(:p_source_rtf);
    tagfound = substring (:p_source_rtf from :tagstart for ((:tagfinish - :tagstart) + 1));
    p_source_rtf = replace (:p_source_rtf, :tagfound, '');
    tagstart = position (:tag_open, :p_source_rtf);
  end

  -- finaliza
  result_text = trim(:p_source_rtf);

  return :result_text;
END

Modo de usar

texto_puro=GET_UNRTF(string_rtf); 

Conclusão

Vale esclarecer que operações com blobs não são performáticas, extrair um plain-text de um blob RTF leva tempo e se você precisa fazer isso num grupo de registros em loop para procurar um CPF, CNPJ ou algo do tipo talvez esteja fazendo isso do jeito errado. Situações que requerem máxima performance, talvez seja melhor pagar um preço maior e adicionar mais um campo para armazenar a mesma versão do conteúdo RTF porém plain-text, é certo que isso quase dobrará o armazenamento, porém em contrapartida terá uma versão RTF analisável já pronta e obterá a melhor performance possível para suas buscas ou analises. Claro que ao usar essa solução, analise se extrairá o plain-text no lado cliente ou usar essa SQL Function no lado servidor, escolha a opção que no seu contexto se encaixar melhor.