#!/bin/env python
#-*- coding: utf-8 -*-

"""PolePDF - Classe PDF em Python que cria um papel e permite adicionar
             células e tabelas encaixando-as ou flutuantes

Arquivo: PolePDF.py
Versão.: 0.1.0
Autor..: Claudio Polegato Junior
Data...: 25 Mar 2011

Copyright © 2011 - Claudio Polegato Junior <junior@juniorpolegato.com.br>
Todos os direitos reservados
"""

import subprocess

from reportlab.lib.pagesizes import A4, legal, landscape
from reportlab.lib.units import cm, mm
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY
from reportlab.lib import colors
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus.paragraph import Paragraph
from reportlab.platypus import Image
from reportlab.lib.styles import ParagraphStyle
from reportlab.platypus.tables import Table
from reportlab.graphics.barcode import getCodeNames as BarCodeNames
from reportlab.graphics.barcode import createBarcodeDrawing as BarCode
from reportlab.graphics.barcode.code128 import Code128


extra_grande_centro = ParagraphStyle('normal', fontSize = 18, leading = 18, alignment = TA_CENTER)
grande_centro       = ParagraphStyle('normal', fontSize = 14, leading = 14, alignment = TA_CENTER)
pequena_esquerda    = ParagraphStyle('normal', fontSize = 5, leading = 5, alignment = TA_LEFT)
normal_esquerda     = ParagraphStyle('normal', fontSize = 8, leading = 8, alignment = TA_LEFT)
pequena_centro      = ParagraphStyle('normal', fontSize = 5, leading = 5, alignment = TA_CENTER)
normal_centro       = ParagraphStyle('normal', fontSize = 8, leading = 8, alignment = TA_CENTER)
pequena_direita     = ParagraphStyle('normal', fontSize = 5, leading = 5, alignment = TA_RIGHT)
normal_direita      = ParagraphStyle('normal', fontSize = 8, leading = 8, alignment = TA_RIGHT)
pequena_justa       = ParagraphStyle('normal', fontSize = 5, leading = 5, alignment = TA_JUSTIFY)
normal_justa        = ParagraphStyle('normal', fontSize = 8, leading = 8, alignment = TA_JUSTIFY)
fonte_tab_pequena   = ParagraphStyle('normal', fontSize = 5, leading = 5, alignment = TA_LEFT)
fonte_tab_grande    = ParagraphStyle('normal', fontSize = 8, leading = 8, alignment = TA_LEFT)
minuscula_direita   = ParagraphStyle('normal', fontSize = 4, leading = 4, alignment = TA_RIGHT)

X = 0
Y = 1
L = 2
A = 3
ALTO = 0
BAIXO = 1

# ' ' = '\xc2\xa0' = nbsp em utf-8

XML_ESCAPE = (('lt', '<'), ('gt', '>'), ('amp', '&'),
              ('quot', '"'), ('nbsp', ' '))

FORM_ESCAPE = (('<b>', '\0b\0'), ('</b>', '\0/b\0'),
               ('<i>', '\0i\0'), ('</i>', '\0/i\0'),
               ('<br/>', '\n'),
               ('&nbsp; ', '  '), (' &nbsp;', '  '))

def escape(text):
    if not text:
        return text
    if text == ' ':
        return '&nbsp;'
    for f, x in FORM_ESCAPE:
        text = text.replace(f, x)
    for e, c in XML_ESCAPE:
        if c == ' ' and isinstance(text, unicode):
            c = u' '
        text = text.replace(c, '&%s;' % e)
    for f, x in FORM_ESCAPE:
        text = text.replace(x, f)
    return text

def unescape(text):
    if not text:
        return text
    for f, x in FORM_ESCAPE:
        text = text.replace(x, f)
    for e, c in XML_ESCAPE:
        text = text.replace('&%s;' % e, c)
    for f, x in FORM_ESCAPE:
        text = text.replace(f, x)
    return text

def paragrafo(texto, estilo):
    return Paragraph(escape(texto), estilo)

class PDF(object):
    def __init__(self, titulo = 'Sem Nome',  nome_do_arquivo = '/tmp/pole.pdf',
                 margem = 1 * cm, tamanho_pagina = A4, espacamento = 0.5 * mm,
                 espessura = 0.5, paisagem = False):
        if paisagem:
            tamanho_pagina = landscape(tamanho_pagina)
        self.canvas = Canvas(nome_do_arquivo, pagesize = tamanho_pagina)
        self.canvas.setTitle(titulo)
        self.titulo = titulo
        self.nome_do_arquivo = nome_do_arquivo
        self.retangulos = [[0, 0,
                            self.canvas._pagesize[0] - 2 * margem,
                            self.canvas._pagesize[1] - 2 * margem]]
        self.margem = margem
        self.espacamento = espacamento
        self.erro = 0.001 * mm
        self.espessura = espessura

    def _print_retangulos(self, retangulos = None):
        for retangulo in retangulos or self.retangulos:
            print ["%0.2f" % (x / cm) for x in retangulo]

    def entre(self, pi, p, pf):
        return (pi - self.erro < p < pf + self.erro)

    def x_entre(self, x, retangulo):
        r = retangulo
        return (r[X] - self.erro < x < r[X] + r[L] + self.erro)

    def y_entre(self, y, retangulo):
        r = retangulo
        return (r[Y] - self.erro < y < r[Y] + r[A] + self.erro)

    def igual(self, p1, p2):
        return (abs(p1 - p2) < self.erro)

    def retangulo_dentro(self, retangulo_menor, retangulo_maior):
        e = self.erro
        Xv, Yv, Lv, Av = retangulo_maior
        Xn, Yn, Ln, An = retangulo_menor
        return (Xv - e < Xn < Xv + Lv + e and
                Xv - e < Xn + Ln < Xv + Lv + e and
                Yv - e < Yn < Yv + Av + e and
                Yv - e < Yn + An < Yv + Av + e)

    def ponto_dentro(self, ponto, retangulo):
        e = self.erro
        Xp, Yp = ponto
        Xr, Yr, Lr, Ar = retangulo
        return (Xr - e < Xp < Xr + Lr + e and
                Yr - e < Yp < Yr + Ar + e)

    def cabe(self, retangulo, largura, altura):
        return (retangulo[L] > largura - self.erro and
                retangulo[A] > altura - self.erro)

    def ampliar(self, qual, retangulo):
        i = +(qual == ALTO) # X se baixo | Y se alto
        j = L + i           # L se baixo | A se alto
        #if retangulo[L + (qual == BAIXO)] < self.erro:
        #    return
        for r in self.retangulos:
            if self.ponto_dentro(retangulo[:2], r):
                retangulo[j] += retangulo[i] - r[i]
                retangulo[i] = r[i]
            if qual == BAIXO and self.ponto_dentro([retangulo[X] +
                                                    retangulo[L],
                                                    retangulo[Y]], r):
                retangulo[L] += r[X] + r[L] - (retangulo[X] + retangulo[L])

    def ponto_insercao(self, largura, altura):
        erro = self.erro
        #~ print '^' * 100
        #~ print 'Requerido:', largura / cm, altura / cm
        #~ print "Retângulos disponíveis"
        #~ self._print_retangulos()
        # Verificar se o retângulo requerido cabe num outro vazio
        coube = False
        for retangulo in self.retangulos:
            coube = self.cabe(retangulo, largura, altura)
            if coube:
                break
        # Se não coube em nenhum vazio, retorna None
        if not coube:
            #~ print "Não coube!"
            return None
        # Retirar o retângulo vazio da lista de disponíveis
        self.retangulos.remove(retangulo)
        # Posição para inserir o novo retângulo
        requerido = retangulo[X], retangulo[Y], largura, altura
        x, y, l, a = requerido
        retorno = (x + self.margem, self.canvas._pagesize[Y] - self.margem - y)
        # Reduzir retângulos disponíveis cruzados pelo requerido e inserir novos restantes
        restantes = []
        for r in self.retangulos:
            if r[Y] > y + altura + self.erro:
                break
            if self.x_entre(x, r) and self.y_entre(r[Y], requerido):
                #~ print 'restante',
                #~ self._print_retangulos([r])
                restantes.append([x + l, r[Y], r[X] + r[L] - (x + l), r[A]])
                r[L] = x - r[X]
            if self.y_entre(y, r) and self.x_entre(r[X], requerido):
                r[A] = y - r[Y]
        self.retangulos += restantes
        #~ print "Quebrados"
        #~ self._print_retangulos()
        # Inserir os dois novos retângulos resultantes do removido
        # Retângulo novo mais alto com o restante da largura e mesma altura
        novo = [x + l, y, retangulo[L] - l, retangulo[A]]
        #~ print "Alto:",
        #~ self._print_retangulos([novo])
        self.ampliar(ALTO, novo)
        #~ print "Alto ampliado:",
        #~ self._print_retangulos([novo])
        self.retangulos.append(novo)
        # Retângulo novo mais baixo com o restante da altura e mesma largura
        novo = [x, y + a, retangulo[L], retangulo[A] - a]
        #~ print "Baixo:",
        #~ self._print_retangulos([novo])
        self.ampliar(BAIXO, novo)
        #~ print "Baixo ampliado:",
        #~ self._print_retangulos([novo])
        self.retangulos.append(novo)
        # Apagar retângulos com altura ou largura zerados
        zeros = [r for r in self.retangulos if r[A] < erro or r[L] < erro]
        #~ print "Zerados"
        #~ self._print_retangulos(zeros)
        for zero in zeros:
            self.retangulos.remove(zero)
        #~ print "Limpo"
        #~ self._print_retangulos()
        # Apagar retângulos disponíveis que estejam dentro de outros
        dentros = []
        for r1 in self.retangulos:
            for r2 in self.retangulos:
                if r1 == r2:
                    continue
                if self.retangulo_dentro(r1, r2):
                    dentros.append(r1)
                    break
        #~ print "Dentros"
        #~ self._print_retangulos(zeros)
        for dentro in dentros:
            self.retangulos.remove(dentro)
        #~ print "Limpo"
        #~ self._print_retangulos()
        # Reordenar retângulos disponíveis por Y, X
        self.retangulos = sorted(self.retangulos, key=lambda x: [x[1], x[0]])

        # retorna o ponto de inserção
        #~ print "Retangulos depois"
        #~ self._print_retangulos()
        return retorno

    def tabela(self, largura, altura, dados, colWidths=None, rowHeights=None, style=None, repeatRows=0, repeatCols=0, splitByRow=1, emptyTableAction=None, ident=None, hAlign=None, vAlign=None):
        # Escape dados str ou unicode
        dados = [[escape(d) if isinstance(d, (str, unicode)) else d for d in l] for l in dados]
        # Renderizar 1 linha
        tabela = Table(dados[:1], colWidths, rowHeights, style, repeatRows, repeatCols, splitByRow, emptyTableAction, ident, hAlign, vAlign)
        l, a = tabela.wrapOn(self.canvas, largura, altura)
        # Quantas linhas dessa altura cabem
        linhas = int(altura/a)
        # Renderizar linhas
        tabela = Table(dados[:linhas], colWidths, rowHeights, style, repeatRows, repeatCols, splitByRow, emptyTableAction, ident, hAlign, vAlign)
        l, a = tabela.wrapOn(self.canvas, largura, altura)
        # Se não couber, vou tirando de 1 em 1
        if a > altura:
            for i in range(linhas, 0, -1):
                tabela = Table(dados[:i], colWidths, rowHeights, style, repeatRows, repeatCols, splitByRow, emptyTableAction, ident, hAlign, vAlign)
                l, a = tabela.wrapOn(self.canvas, largura, altura)
                if a <= altura:
                    break
            return (tabela, i, a)
        # Se couber, vou colocando de 1 em 1
        i = linhas
        for i in range(linhas, len(dados)):
            tabela = Table(dados[:i], colWidths, rowHeights, style, repeatRows, repeatCols, splitByRow, emptyTableAction, ident, hAlign, vAlign)
            l, a = tabela.wrapOn(self.canvas, largura, altura)
            if a > altura:
                break
        i -= 1
        tabela = Table(dados[:i], colWidths, rowHeights, style, repeatRows, repeatCols, splitByRow, emptyTableAction, ident, hAlign, vAlign)
        l, a = tabela.wrapOn(self.canvas, largura, altura)
        return (tabela, i, a)

    def celula(self, largura, altura, titulo, dados, borda = True, arredondamento = 0.5 * mm, ajuste_inferior = 0 * mm, espacamento = None, posicao = None, espessura = None, alinhamento = 'justa', multilinha = None):
        if espessura == None:
            espessura = self.espessura
        if espacamento == None:
            espacamento = self.espacamento * 2
            #if titulo is None:
            #    tipos = [type(x) for x in dados]
            #    if not (str in tipos or unicode in tipos or Paragraph in tipos):
            #        espacamento = 0
        else:
            espacamento *= 2
        if alinhamento.lower() == 'esquerda':
            estilo = normal_esquerda
        elif alinhamento.lower() == 'direita':
            estilo = normal_direita
        elif alinhamento.lower() == 'centro':
            estilo = normal_centro
        else:
            estilo = normal_justa
        if titulo == '':
            componentes = (paragrafo(dados, estilo),)
        elif titulo:
            componentes = (paragrafo(titulo, pequena_esquerda), paragrafo(dados, estilo))
        else:
            componentes = dados
        altura_total = 0
        altura_texto = altura - espacamento + ajuste_inferior
        largura_texto = largura - espacamento
        renderizados = []
        if not multilinha and titulo and dados != '&nbsp;':
            componentes[1].wrapOn(self.canvas, largura_texto, altura_texto)
            linhas = componentes[1].breakLines(largura_texto).lines
            if len(linhas) > 0:
                if isinstance(linhas[0], tuple): # texto comum
                    componentes = (componentes[0], paragrafo(' '.join(linhas[0][1]), estilo))
                else: # texto com formatação
                    componentes = (componentes[0], paragrafo(''.join(w.text for w in linhas[0].words), estilo))
        for componente in componentes:
            larg, alt = componente.wrapOn(self.canvas, largura_texto, altura_texto)
            altura_total += alt
            altura_texto -= alt
            renderizados.append((altura_total, componente, larg))
        altura_total -= ajuste_inferior
        if altura_total < altura - espacamento:
            altura_total = altura - espacamento
        if posicao:
            x, y = posicao
            y = self.canvas._pagesize[1] - y
        else:
            pi = self.ponto_insercao(largura, altura_total + espacamento)
            if pi:
                x, y = pi
            else:
                print ('Um objeto de %i cm x %i cm não coube na página do PDF "%s" e será apresentado'
                       'no canto inferior direito desta página' % (largura / cm, altura_total / cm, self.titulo))
                x = self.canvas._pagesize[0] - largura - espessura / 2
                y = altura_total + espacamento + espessura / 2
        #print "Posição:", x/mm, y/mm
        for renderizado in renderizados:
            self.canvas.setLineWidth(espessura)
            #renderizado[1].drawOn(self.canvas, x + espacamento / 2, y - renderizado[0] - espacamento / 2)
            renderizado[1].drawOn(self.canvas, x + (largura - renderizado[2]) / 2, y - renderizado[0] - espacamento / 2)
        if borda:
            self.canvas.roundRect(x, y - altura_total - espacamento, largura, altura_total + espacamento, arredondamento)
        return (x, y, largura, altura_total + espacamento)

    def altura(self, largura, altura, titulo, dados, borda = True, arredondamento = 0.5 * mm, ajuste_inferior = 0 * mm, espacamento = None, posicao = None, espessura = None, alinhamento = 'justa', multilinha = None):
        if espessura == None:
            espessura = self.espessura
        if not espacamento:
            espacamento = self.espacamento * 2
        else:
            espacamento *= 2
        if alinhamento.lower() == 'esquerda':
            estilo = normal_esquerda
        elif alinhamento.lower() == 'direita':
            estilo = normal_direita
        elif alinhamento.lower() == 'centro':
            estilo = normal_centro
        else:
            estilo = normal_justa
        if titulo == '':
            componentes = (Paragraph(dados, estilo),)
        elif titulo:
            componentes = (Paragraph(titulo, pequena_esquerda), Paragraph(dados, estilo))
        else:
            componentes = dados
        altura_total = 0
        altura_texto = altura - espacamento + ajuste_inferior
        largura_texto = largura - espacamento
        renderizados = []
        if not multilinha and titulo and dados != '&nbsp;':
            componentes[1].wrapOn(self.canvas, largura_texto, altura_texto)
            linhas = componentes[1].breakLines(largura_texto).lines
            if len(linhas) > 0:
                if isinstance(linhas[0], tuple): # texto comum
                    componentes = (componentes[0], Paragraph(' '.join(linhas[0][1]), estilo))
                else: # texto com formatação
                    componentes = (componentes[0], Paragraph(''.join([linhas[0].words[i].text for i in range(len(linhas[0].words))]), estilo))
        for componente in componentes:
            larg, alt = componente.wrapOn(self.canvas, largura_texto, altura_texto)
            altura_total += alt
            altura_texto -= alt
            renderizados.append((altura_total, componente, larg))
        altura_total -= ajuste_inferior
        if altura_total < altura - espacamento:
            altura_total = altura - espacamento
        return altura_total + espacamento

    def tamanho(self, componente, largura_maxima):
        return componente.wrapOn(self.canvas, largura_maxima, 1)

    def salvar(self):
        self.canvas.save()

    def mostrar(self, programa = 'xdg-open'):
        self.canvas.save()
        subprocess.Popen([programa, self.nome_do_arquivo])

    def nova_pagina(self):
        self.canvas.showPage()
        self.retangulos = [(0, 0, self.canvas._pagesize[0] - 2 * self.margem, self.canvas._pagesize[1] - 2 * self.margem)]

if __name__ == "__main__":
    import cStringIO, base64
    LOGO_PYTHON = ' '
    logo = cStringIO.StringIO()
    logo.write(base64.b64decode(LOGO_PYTHON))
    logo.seek(0)
    # Criar instância do PDF
    pdf = PDF()
    componente = Paragraph('Junior&nbsp;Polegato', normal_direita)
    l, a = pdf.tamanho(componente, 1)
    l += 2 * pdf.espacamento
    a += 2 * pdf.espacamento + .3 * a
    pdf.celula( l, a, None, [componente], posicao = (20 * cm - l, 28.7 * cm - a), borda= True)
    pdf.celula( 5 * cm, 1 * cm, 'Teste', '1')
    pdf.celula(15 * cm, 2 * cm, 'Teste', '2')
    pdf.celula( 4 * cm, 5 * cm, 'Teste', '3')
    pdf.celula(10 * cm, 1 * cm, 'Teste', '4')
    pdf.celula( 5 * cm, 5 * cm, 'Teste', '5')
    pdf.celula( 4 * cm, 3 * cm, 'Teste', '6')
    pdf.celula( 7 * cm, 1 * cm, 'Teste', '7')

    # Renderiza uma imagem
    img = Image(logo)
    largura = 5 * cm   # redimensionar imagem para 5 cm de largura
    img.drawHeight = largura * img.drawHeight / img.drawWidth
    img.drawWidth = largura
    pdf.celula(largura, 1, None, [img])
    # Renderiza uma célula texto com título e outra sem título
    pdf.celula(10 * cm, 1, 'Título da célula', 'Texto de conteúdo da célula')
    pdf.celula(10 * cm, 1, '', 'Texto de conteúdo da célula sem título')
    # Renderiza uma tabela simples
    estilo = [('GRID', (0,0), (-1,-1),0.5, colors.black),
              ('VALIGN',(0,0),(-1,-1),'MIDDLE'),
              ('TOPPADDING', (0,0), (-1,-1), 0),
              ('RIGHTPADDING', (0,0), (-1,-1), 1*mm),
              ('LEFTPADDING', (0,0), (-1,-1), 1*mm),
              ('FONTSIZE', (0,0), (-1,-1), normal_centro.fontSize),
              ('LEADING', (0,0), (-1,-1), normal_centro.leading),
              ('BOTTOMPADDING', (0,0), (-1,-1), 1)]
    dados = [[123, 'Hora 1', 5465, 123465, 1, 2, 3, 4, True] ,
             [123, 'Hora 2', 5465, 123465, 1, 2, 3, 4, False],
             [123, 'Hora 3', 5465, 123465, 1, 2, 3, 100, True] ,
             [123, 'Hora 4', 5465, 123465, 1, 2, 3, 4, True]] * 25
    tab, i, h = pdf.tabela(19 * cm, 3 * cm, dados, style = estilo) # Não renderiza
    w = sum(tab._colWidths) + 0.5 * cm
    pdf.celula(w, h, None, [Paragraph('Tabela', pequena_esquerda), tab, Paragraph('&nbsp;', pequena_esquerda)])
    # Renderizar códigos de barras
    for code_name in BarCodeNames():
        if code_name == 'FIM':
            value = 'C'
        elif code_name == 'POSTNET':
            value = '123456789'
        elif code_name == 'USPS_4State':
            value = '01234567094987654321'
        elif code_name == 'QR':
            value = 'Junior Polegato - Utilização do PolePDF para geração fácil de PDF de uso diário.'
        else:
            value = '7899041283458'
        bar = BarCode(code_name, value = value)
        pdf.celula(9 * cm, 1, None, [Paragraph(code_name, pequena_esquerda), bar])
    pdf.celula(1 * cm, 2 * cm, 'Onde estou', 'Fim')
    pdf.mostrar()
