Automatizando diagramação de notícias com Scribus e Python

Matéria montada automaticamente pelo script.

Matéria montada automaticamente pelo script.

Que tal abrir um quadro de texto no Scribus, rodar um script e obter rapidamente uma matéria jornalística montada, com foto, legenda e crédito da foto? E que tal levar todo um sistema editorial num pen drive? (Editor de texto, editor de fotos, paginador, saída em PDF/X-3). E que tal tudo isto com ferramentas gratuitas e de código aberto?

Essa foi minha idéia ao aprender Python e Scribus Scripting. Levar um sistema editorial de categoria profissional num pen drive. Instalei o X-Scribus, um Scribus especial para pen drives. Ele já tem o Ghostscript, Python e tudo o que é necessário para rodar em qualquer computador com Windows. Existe mais uma versão, o Portable Scribus 1.3.3.12, mas ele me apresentou problemas na visualização de impressão: não permitia a seleção de chapas (plates) individuais — ciano, magenta, amarelo ou preto, coisa importante para o ensino de seleção de cores.

Imagens

Para automatizar o Scribus, uma das primeiras providências é permitir ao Python do Scribus a manipulação de imagens. Para isso, usei PIL (Python Image Library).

No Linux, o Scribus usa o Python normal do sistema. Instale as bibliotecas PIL da forma usual de seu sabor de Linux. No Windows, o instalador do Python Image Library precisa de certas chaves no registro do Windows para saber em que lugar está instalado o sistema Python. Eu defini estas chaves para a pasta do Scribus, que é onde roda o interpretador Python no Windows. Ao baixar o PIL, cuide as versões: no Scribus versão 1.3.3 estável,  o Python é versão 2.4. No Scribus 1.3.4 Beta, o Python é versão 2.5.

Para registrar o Python do Scribus, o meu arquivo reg tem o seguinte:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Python]
[HKEY_LOCAL_MACHINE\SOFTWARE\Python\Pythoncore]
[HKEY_LOCAL_MACHINE\SOFTWARE\Python\Pythoncore\2.4]
[HKEY_LOCAL_MACHINE\SOFTWARE\Python\Pythoncore\2.4\InstallPath]
@="c:\\SD\\PortableApps\\X-Scribus\\Bin\\Scribus\\"
[HKEY_LOCAL_MACHINE\SOFTWARE\Python\Pythoncore\2.4\PythonPath]
@="c:\\SD\\PortableApps\\X-Scribus\\Bin\\Scribus\\Lib\\;c:\\SD\\PortableApps\\X-Scribus\\Bin\\Scribus\\DLLs\\"

[HKEY_LOCAL_MACHINE\SOFTWARE\Python\Pythoncore\2.5]
[HKEY_LOCAL_MACHINE\SOFTWARE\Python\Pythoncore\2.5\InstallPath]
@="c:\\SD\\Bin\\Scribus\\"
[HKEY_LOCAL_MACHINE\SOFTWARE\Python\Pythoncore\2.5\PythonPath]
@="c:\\SD\\Bin\\Scribus\\Lib\\;c:\\SD\\Bin\\Scribus\\DLLs\\"

Se você precisar registrar, copie este código, mude os caminhos para as pastas para casarem com seu sistema (em vermelho) e grave como texto simples, com a terminação .reg. Dê dois clics no arquivo para registrar as chaves no registro do Windows.

Depois de instalado o PIL, se as bibliotecas de imagem forem corretamente reconhecidas, não haverá erros ao rodar o script de montagem de matérias.

Repare que instalei tudo usando o HD. Depois de tudo instalado, copiei a pasta inteira para o pen drive. No caso, copiei a pasta X-Scribus para a pasta PortableApps (que é o padrão de aplicações portáteis que uso) do pen drive.

A seguir coloquei o script na pasta de scripts do Scribus, que no meu caso coloquei em c:\SD\PortableApps\X-Scribus\Bin\Scribus\Scripts.

No início do script existem algumas variáveis que podem ser modificadas pelo usuário, como a largura da valeta (gutter, espaço entre as colunas).

O script funciona assim: desenhe um retângulo ou selecione um. Vá ao menu Scripts, procure e rode este script. Aparecem diversas janelinhas de diálogo perguntando:

  • Em quantas colunas será dividida a área de colunas da matéria? Escolha 2 ou mais.
  • Qual a largura do espaço entrecolunas (gutter ou valeta)?
  • Qual o corpo (tamanho) do sobretítulo?
  • Qual o corpo do subtítulo?
  • Quantas fotos tem a matéria? Por enquanto, o script só aceita uma.
  • Quantas colunas ocupa a foto (de largura)? A foto é posicionada ao lado da primeira coluna de texto e agrupada com quadros para legenda e para crédito. As bibliotecas PIL são usadas para isto. As funções PIL medem a foto escolhida e o script calcula a altura do quadro de foto. Mais tarde, uso a tecla CTRL + Y para abrir o editor de texto interno do Scribus para editar os dois elementos (como se faz no Adobe inDesign). Na primeira linha vai a legenda e na linha de baixo vai o crédito.

Falta fazer as rotinas para manipular fotos no caso de uma matéria em apenas uma coluna. Se você escolher uma coluna, a foto será montada ao lado da matéria.

A seguinte seqüência de quadros mostra como funciona o script.

O quadro para título vai agrupado com o quadro para as colunas. Para editar o texto, uso a tecla CTRL + Y. Para importar texto, seleciono o grupo e uso CTRL + D (como no PageMaker e InDesign).

#!/usr/bin/env python2.4
# -*- coding: utf-8 -*-

"""
Build a newspaper story
By prof. MS. José Antonio Meira da Rocha
mailto:joseantoniorocha@gmail.com

http://meiradarocha.jor.br

Licença GPL
"""

import sys

###########################
# Assegura que está rodando
# dentro do Scribus
###########################
try:
	import scribus
except ImportError,err:
	print "This Python script is written for the Scribus scripting interface."
	print "It can only be run from within Scribus."
	sys.exit(1)

# Carrega algumas constantes Scribus usadas neste script
from scribus import UNIT_POINTS, LINE_SOLID, JOIN_MITTER, BUTTON_OK, ICON_WARNING

#########################
# USER IMPORTS GO HERE  #
#########################

#####################################
# Usuário pode mudar estes parâmetros
#####################################
numeroDeColunasPadrao = "3"
entrecolunasPadrao = "14.1732"  # 5mm
corpoDeTituloPadrao = "48"
corpoPadraoDoSobretitulo = "18"
proporcaoDeEntrelinha = 1.0

##########################################
# Constantes que NÃO devem ser modificadas
AROUND_FRAME = 1
BOUNDING_BOX = 2
CONTOUR_LINE = 3 

######################
# Locale strings
# Textos para tradução
######################
pedirParaAbrirDoc = "<h2>Abra um documento</h2>" \
	+"Ops! Abra um documento antes \nde rodar este comando."
pedirParaAbrirQuadro = "<h2>Desenhe um quadro</h2>\n" \
	+"Ops! Desenhe ou selecione um <b>quadro</b>\n" \
	+"na área em que você quer montar a matéria."
#
labelDeColunas = 'Colunas' #Number of columns
mensagemDeColunas = '<h2>Colunas</h2><p>Quantas colunas?</p>'  #How many columns?
#
labelDeCorpoDeTitulo = "Corpo do título" #quadroDeTitulo
mensagemDeCorpoDeTitulo = "<h2>Corpo do título</h2><p>Qual&nbsp;o&nbsp;corpo&nbsp;do&nbsp;título?</p>" #What the quadroDeTitulo size?
#
labelDeValetas = "Valetas"
mensagemDeValetas = "<h2>Valetas</h2><p>Qual&nbsp;o&nbsp;tamanho&nbsp;da&nbsp;valeta?\n(Espaço&nbsp;entrecolunas)"
#
labelDeSobretitulo = "Corpo do sobretítulo"
mensagemDeSobretitulo = "<h2>Corpo do sobretítulo</h2>\n<p><b>Qual&nbsp;o&nbsp;corpo&nbsp;do&nbsp;sobretítulo?</b>\n" \
	+ "(coloque&nbsp;zero&nbsp;se&nbsp;não&nbsp;houver&nbsp;sobretítulo)"
#
textFlowsAroundFrame = 'Abra a imagem'
filtroDeImagens = 'Arquivos de imagens (*.jpg *.png *.tiff *.tif)'
#
labelDeImportarTexto = "Abra o texto"
filtroDeArquivosTexto = "Arquivos de texto (*.txt *.html)"
#
labelDeLarguraDeImagem = "Largura da imagem"
mensagemDeLarguraDeImagem = "<h2>Largura&nbsp;da&nbsp;Imagem</h2>\nQuantas&nbsp;colunas&nbsp;a&nbsp;imagem&nbsp;ocupará?"
larguraDeColunasPadrao = "1"
#
labelDeImagem = "Quantas fotos"
mensagemDeImagem = "<h2>Quantas fotos?</h2>\n<p>Quantas&nbsp;fotos&nbsp;a&nbsp;matéria&nbsp;tem?"
numeroDeImagemPadrao = "1"

labelDeQuadroDeTitulo = "Montar matéria"

labelErroDeImportacao = "Não importei o arquivo"
msgDeErroDeImportacao = "<h2>Não importei o arquivo</h2><p>Desculpe-me, não importei o arquivo por algum detalhe de formato incompatível ou arquivo não encontrado. <b>Sem problema</b>, tente de novo manualmente nais tarde.</p>"

labelAbreDoc = "Abra um documento"
dizAbreDoc = "<h2>Abra um documento</h2><p>Por favor, este comando exige que você abra um documento.</p>"

####################
# End Locale strings
####################

try:
	import Image
except ImportError,err:
	print "This Python script is written for the PIL graphic interface."
	print "It should be instaled in Scribus Python tree."
	sys.exit(1)

def pedeNumeroDeColunas():
	"""Pede número de colunas de texto que terá a matéria"""

	numeroDeColunas = scribus.valueDialog(
		labelDeColunas,
		mensagemDeColunas,
		numeroDeColunasPadrao
	)
	if not numeroDeColunas:
		sys.exit()
	try:
		return int(numeroDeColunas)
	except:
		return numeroDeColunasPadrao

def pedeCorpoDoTitulo():
	"""Pede tamanho das letras (corpo) do título, em pontos tipográficos."""
	corpoDeTitulo = scribus.valueDialog(labelDeCorpoDeTitulo, mensagemDeCorpoDeTitulo, corpoDeTituloPadrao)
	if not corpoDeTitulo:
		sys.exit()
	try:
		return float(eval(corpoDeTitulo))
	except:
		return float(corpoDeTituloPadrao)

def pedeEntrecolunas():
	"""Pede o espaço entre as colunas de texto (gitter, valeta), em pontos tipográficos"""
	entrecolunas = scribus.valueDialog(labelDeValetas, mensagemDeValetas, entrecolunasPadrao)
	if not entrecolunas:
		sys.exit()
	try:
		return float(entrecolunas)
	except:
		return entrecolunasPadrao

def pedeCorpoDoSobretitulo():
	"""Pede tamanho das letras (corpo) do sobretítulo, em pontos tipográfico"""
	corpoDoSobretitulo = scribus.valueDialog(labelDeSobretitulo, mensagemDeSobretitulo, corpoPadraoDoSobretitulo)
	if not corpoDoSobretitulo:
		sys.exit()
	try:
		return float(eval(corpoDoSobretitulo))
	except:
		return float(corpoPadraoDoSobretitulo)

def pedeImagem():
	"""Pergunta por fotos na matéria"""
	temImagem = scribus.valueDialog(labelDeImagem, mensagemDeImagem, numeroDeImagemPadrao)
	if not temImagem:
		sys.exit()
	try:
		return temImagem
	except:
		return temImagem

def pedeTexto(quadroDeTitulo):
	"""Load text file"""

	sourceCharcode = 'iso-8859-15' # change it to ur language
	# Diálogo de procurar arquivo
	textFile = scribus.fileDialog(labelDeImportarTexto,filtroDeArquivosTexto,"", haspreview=1, issave=True) # issave=False shows "Save" button
	if textFile:
		try:
			t = open(textFile).read()
			t = unicode(t, sourceCharcode)
		except:
			scribus.messageBox(
				labelErroDeImportacao,
				msgDeErroDeImportacao,
				ICON_WARNING,
				BUTTON_OK
			)
			t=""
			return t
	else:
		t=""

	return t

def flow(frame,mode):
	"""Compatibiliza Scribus 1.3.3 e 1.3.4"""
	try:
		scribus.textFlowsAroundFrame(frame, mode) # Scribus 1.3.3
	except:
		scribus.textFlowMode(frame, mode)		 # Scribus 1.3.4

def pedeColunasDeImagem():
	"""Solicita a quantidade de colunas que a foto vai ocupar."""
	colunasDaFoto = scribus.valueDialog(
	labelDeLarguraDeImagem,
	mensagemDeLarguraDeImagem,
	larguraDeColunasPadrao
	)
	if not colunasDaFoto:
		sys.exit()
	try:
		return float(eval(colunasDaFoto))
	except:
		return float(larguraDeColunasPadrao)

def pedeFoto():
	"""	Escolhe arquivo de foto
	scribus.fileDialog('caption', ['filter', 'defaultname',haspreview, issave])"""

	# Abre diálogo para escolha de arquivo
	arquivoDaFoto = scribus.fileDialog(
		textFlowsAroundFrame,
		filtroDeImagens,
		"",  # Nome default do arquivo
		haspreview=1,
		issave=True
	)

	# Tenta abrir arquivo fornecido
	try:
		foto = Image.open(arquivoDaFoto)  # uso da biblioteca PIL
	except:
		scribus.messageBox(
			labelErroDeImportacao,
			msgDeErroDeImportacao,
			ICON_WARNING,
			BUTTON_OK
		)
		foto = ""
	return foto,arquivoDaFoto

def montaQuadroDaFoto(quadroDeColunas):
	"""Build a image frame with credit and legenda
	based in text frame number of columns"""

	#
	foto,arquivoDaFoto = pedeFoto()
	try:
		larguraDaFoto,alturaDaFoto = foto.size  # para pegar o tamanho da imagem em pixels
	except:
		larguraDaFoto,alturaDaFoto = 4,3  # Se não conseguir, define proporção 4 por 3

	# Escolhe quantas colunas a foto vai "abrir"
	colunasDaFoto = pedeColunasDeImagem()

	# Calcula largura da foto
	entrecoluna = scribus.getColumnGap(quadroDeColunas)
	colunasDeTexto = scribus.getColumns(quadroDeColunas)

	# Define o tamanho da foto
	larguraDoQuadroDeColunas,alturaDoQuadroDeColunas = scribus.getSize(quadroDeColunas)

	larguraDaColuna = (larguraDoQuadroDeColunas - (entrecoluna*(colunasDeTexto-1))) / colunasDeTexto

	# calcula posição da imagem
	quadroDeColunasEsquerda, quadroDeColunasTopo = scribus.getPosition(quadroDeColunas)

	# Posiciona foto a partir da segunda coluna de texto
	quadroDaFotoEsquerda = quadroDeColunasEsquerda + larguraDaColuna + entrecoluna
	quadroDaFotoTopo = quadroDeColunasTopo

	# Calcula largura e altura da imagem
	larguraDoQuadroDaFoto = colunasDaFoto * larguraDaColuna + (entrecoluna * (colunasDaFoto - 1))
	alturaDoQuadroDaFoto = larguraDoQuadroDaFoto * alturaDaFoto / larguraDaFoto

	# Cria quadro de imagem com a foto
	quadroDaFoto = scribus.createImage(quadroDaFotoEsquerda, quadroDaFotoTopo, larguraDoQuadroDaFoto, alturaDoQuadroDaFoto)

	# Define fluxo para "Afastar texto"
	flow(quadroDaFoto, AROUND_FRAME) 

	# Define formatação gráfica do quadro de foto
	scribus.setLineWidth(	   0.8, quadroDaFoto)
	scribus.setLineColor(   "Black", quadroDaFoto)
	scribus.setLineShade(	   100, quadroDaFoto)
	scribus.setLineStyle(LINE_SOLID, quadroDaFoto)
	scribus.setLineJoin(JOIN_MITTER, quadroDaFoto)
	try:
		scribus.loadImage(arquivoDaFoto, quadroDaFoto)
	except:
		print "Não pude carregar a imagem."

	# Calcula escala da imagem para caber no quadro
	escalaDaFoto = larguraDoQuadroDaFoto / larguraDaFoto

	# Resescalona quadro de imagem
	scribus.scaleImage(escalaDaFoto, escalaDaFoto, quadroDaFoto)

	########################################
	# LEGENDA
	########################################
	# Calcula dimensões do quadro de legenda
	quadroDaLegendaTopo = quadroDaFotoTopo + alturaDoQuadroDaFoto
	alturaDoQuadroDaLegenda = entrecoluna * 2
	# Cria quadro de legenda
	quadroDaLegenda = scribus.createText(quadroDaFotoEsquerda, quadroDaLegendaTopo, larguraDoQuadroDaFoto, alturaDoQuadroDaLegenda)
	#
	# Melhoria: Colocar leitor de input para escrever legenda

	# Liga "afastador de texto"
	flow(quadroDaLegenda, AROUND_FRAME)

	##########################################
	# CREDITO
	##########################################
	# Define medidas do quadro para crédito de foto
	quadroDaFotoDireita = quadroDaFotoEsquerda + larguraDoQuadroDaFoto
	quadroDaFotoBaixo = quadroDaFotoTopo + alturaDoQuadroDaFoto
	# Cria quadro de crédito
	quadroDoCredito = scribus.createText(quadroDaFotoDireita, quadroDaFotoBaixo, alturaDoQuadroDaFoto, entrecoluna)
	#
	# Melhoria: Colocar input de crédito para escrever credito
	#   if not finalizado
	#	  processaNaoFinalizado()

	# Liga "afastador de texto"
	flow(quadroDoCredito, AROUND_FRAME)

	# Agrupa os elementos foto, legenda e crédito
	scribus.groupObjects([quadroDaFoto, quadroDaLegenda, quadroDoCredito])

	# Se tiver a legenda e crédito no mesmo arquivo que o texto da matéria
	#scribus.linkTextFrames(quadroDeTitulo, quadroDaLegenda)

	# Liga os textos da legenda e dos créditos
	scribus.linkTextFrames(quadroDaLegenda, quadroDoCredito)

	# Se tiver a legenda e crédito no mesmo arquivo que o texto da matéria
	#scribus.linkTextFrames(credit, quadroDeColunas)

	# Deixa o quadro de legenda "em pé"
	scribus.rotateObject(90, quadroDoCredito)

def montaMateria(espacoDaMateria):
	""" Manipula um bloco de notícia """

	# Pega as coordenadas X e Y do objeto selecionado
	esquerdaDoQuadro, topoDoQuadro = scribus.getPosition(espacoDaMateria)
	# Pega a largura e altura do quadro
	larguraDoQuadro, alturaDoQuadro = scribus.getSize(espacoDaMateria)
	# Apaga objeto original, que era usado só pra marcar o espaço.
	scribus.deleteObject(espacoDaMateria)

	# Cria um bloco de texto com as medidas extremas do objeto
	# E deleta o objeto (que pode ser até uma linha)
	quadroDeTitulo = scribus.createText(esquerdaDoQuadro, topoDoQuadro, larguraDoQuadro, alturaDoQuadro)

	# Pergunta pelo número de colunas
	numeroDeColunas = pedeNumeroDeColunas()

	# Pergunta pelo espaço entre-colunas
	entrecolunas = pedeEntrecolunas()

	# Pergunta pelo tamanho do título em pontos
	corpoDeTitulo = pedeCorpoDoTitulo()

	# Pergunta pelo tamanho do antetítulo
	corpoDoSobretitulo = pedeCorpoDoSobretitulo()

	#######################################
	# TÍTULO
	#######################################
	# Calcula o tamanho total dos títulos
	alturaDoQuadroDeTitulo = (corpoDeTitulo + corpoDoSobretitulo) * proporcaoDeEntrelinha
	direitaDoQuadro = esquerdaDoQuadro + larguraDoQuadro
	# Redimensiona quadro de título
	scribus.sizeObject(larguraDoQuadro, alturaDoQuadroDeTitulo, quadroDeTitulo)
	# Define o tipo de fluxo de texto: "afastar texto"
	flow(quadroDeTitulo, AROUND_FRAME)

	#######################################
	# COLUNAS DE TEXTO
	#######################################
	# Define medidas do quadro de textos
	topoDasColunas = topoDoQuadro + alturaDoQuadroDeTitulo
	alturaDasColunas = alturaDoQuadro - alturaDoQuadroDeTitulo
	# Cria quadro de texto
	quadroDeColunas = scribus.createText(esquerdaDoQuadro, topoDasColunas, larguraDoQuadro, alturaDasColunas)
	# Define o fluxo de texto como "afastar texto"
	flow(quadroDeColunas, AROUND_FRAME)
	# Determina colunagem
	scribus.setColumnGap(entrecolunas, quadroDeColunas)
	scribus.setColumns(numeroDeColunas, quadroDeColunas)

	#######################################
	# LINCA TEXTOS
	# Linca quadros de título e de texto
	#######################################
	scribus.linkTextFrames(quadroDeTitulo,quadroDeColunas)
	storyGroup = scribus.groupObjects([quadroDeTitulo, quadroDeColunas])

	#######################################
	# PEGA TEXTO
	#######################################
	# Quadro de escolha de arquivo
	text = pedeTexto(quadroDeTitulo)

	try:
		#scribus.insertText(text,-1,quadroDeTitulo)  # para ANEXAR ao final em vez de substituir
		scribus.setText(text,quadroDeTitulo)
	except:
		text="SobretítulonTítulonAutor da matérianComplemento ao autornPrimeiro ParágrafonOutros parágrafosOutros parágrafos..."
		scribus.setText(text,quadroDeTitulo)

	#######################################
	# PEGA FOTO
	#######################################
	# Ask image
	temImagem = pedeImagem()
	if temImagem:
		montaQuadroDaFoto(quadroDeColunas)

def manipulaSelecao():
	""" Gerencia objetos selecionados """

	# Pega o objeto selecionado
	story = scribus.getSelectedObject(0)
	# Se há apenas um objeto selecionado
	if story and scribus.selectionCount() == 1 :
	#	if story:
		montaMateria(story)
		scribus.docChanged(True)
	else:
		#Melhoria pra o futuro:
		#por default, abrir uma matéria do tamanho da página
		# De momento, só avisa
		scribus.messageBox(
			labelDeQuadroDeTitulo,
			pedirParaAbrirQuadro,
			ICON_WARNING,
			BUTTON_OK
		)

def manipulaDocumento():
	"""Manipula documentos """
	# Se há documento aberto
	if scribus.haveDoc():
		#Desliga redraw
		scribus.setRedraw(False)
		#Guarda unidades do usuário # Save unit
		unit = scribus.getUnit()
		#Define novas unidades como "pontos tipográfico"
		scribus.setUnit(UNIT_POINTS)

		#############################
		# Manipula objeto selecionado
		#############################
		manipulaSelecao()

		#Recupera medidas do usuário
		scribus.setUnit(unit)
	else:  # Senão há documento aberto, avisa.
		scribus.messageBox(
			labelAbreDoc,
			dizAbreDoc,
			ICON_WARNING,
			BUTTON_OK
		)

def myCode():
	""" User code """
	#########################
	#  USER CODE GOES HERE  #
	#########################

	# Gerencia documento
	manipulaDocumento()

	#########################
	#  USER CODE ENDS HERE  #
	#########################

def main(argv):
	"""Default main entry point"""
	myCode()

def main_wrapper(argv):
	try:
		scribus.statusMessage("Rodando o script...")
		scribus.progressReset()
		main(argv)
	finally:
		if scribus.haveDoc():
			scribus.setRedraw(True)
		scribus.statusMessage("")
		scribus.progressReset()

if __name__ == '__main__':
	main_wrapper(sys.argv)

Bibliografia

  1. ROSSUM, Guido Van. Python Tutorial. Capítulo 4, More control flow tools, FOR statements. Site web disponível em: <http://www.python.org/doc/2.4.4/tut/node6.html#SECTION006200000000000000000>. Acesso em 25. jul. 2008.
  2. SCRIBUS. Arquivo de ajuda. Versão 1.3.3.12. Capítulo For Developers, seção Scripter API, página Page Comands. Disponível no programa através da tecla F1.

Sobre José Antonio Rocha

Jornalista, professor de Planejamento Gráfico e Mídias Digitais da Universidade Federal de Santa Maria, campus da cidade de Frederico Westphalen, Rio Grande do Sul, Brasil.