Module ImageASCII
[hide private]
[frames] | no frames]

Source Code for Module ImageASCII

  1  # -*- coding:latin-1 -*- 
  2  """ 
  3  Copyright 2009 Sébastien L. W. Baelde (contact : clic4@clic4.org) 
  4   
  5      This file is part of ImASCII. 
  6   
  7      ImASCII is free software: you can redistribute it and/or modify 
  8      it under the terms of the GNU General Public License as published by 
  9      the Free Software Foundation, either version 3 of the License, or 
 10      any later version. 
 11   
 12      ImASCII is distributed in the hope that it will be useful, 
 13      but WITHOUT ANY WARRANTY; without even the implied warranty of 
 14      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 15      GNU General Public License for more details. 
 16   
 17      You should have received a copy of the GNU General Public License 
 18      along with ImASCII.  If not, see <http://www.gnu.org/licenses/>. 
 19  """ 
 20   
 21  from PIL import Image, ImageDraw, ImageFont 
 22  from PyRTF import Document, Section, Renderer, Font, TEXT, LINE, Paragraph, Colour 
 23  import math, os 
 24   
25 -def OpenFile( name ) :
26 return file( '%s.rtf' % name, 'w' )
27
28 -class Vecteur2D:
29 - def __init__ (self, x, y):
30 self.x = x 31 self.y = y
32
33 -class Couleur:
34 """ Classe gérant une couleur. """
35 - def __init__(self, r, v, b):
36 """ 37 Constructeur. 38 39 @param r: Couleur rouge (0-255) 40 @param v: Couleur vert (0-255) 41 @param b: Couleur bleu (0-255) 42 """ 43 self.r = int(r) 44 self.v = int(v) 45 self.b = int(b)
46
47 - def __str__(self):
48 return "r: "+ str(self.r) + " v: " + str(self.v) + " b: " + str(self.b)
49
50 - def __eq__ (self, coul):
51 52 #if coul.r == self.r and coul.v == self.v and coul.b == self.b: 53 # Tolérance sur l'égalité de couleur. Diminue le nombre de couleurs. 54 if coul.r <= self.r + 35 and coul.r >= self.r - 35: 55 if coul.v <= self.v + 35 and coul.v >= self.v - 35: 56 if coul.b <= self.b + 35 and coul.b >= self.b - 35: 57 return True 58 else: 59 return False
60
61 -class ImageASCII:
62 """ 63 Classe pour effectuer la conversion d'une image en texte. 64 """
65 - def __init__(self, parent):
66 """ 67 Construction d'une table de référence pour les caractères comprenant la valeur moyenne pour chaque lettre. 68 Un espace est équivalent à un poids de 1.0 tandis qu'un caractère complètement noir aurait la valeur de 0.0 69 70 @param parent: Fenêtre parent. 71 """ 72 self.parent = parent 73 self.listeCaracteresA = ['.', ' ', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 74 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 75 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 76 'X', 'Y', 'Z', ',', ';', ':', '?', '!', '+', '-', '*', '&', '/', '=', '(', ')', '<', 77 '>', '_', '[', ']', '{', '}', 'ä', '^', '|', 'ç', '§', '°', '#', '@', '%', '0', 78 '1', '2', '3', '4', '5', '6', '7', '8', '9', '\'', '\"', '~', '¢', '¬', '£', 'é', 'è'] 79 self.listeCaracteresB = [' ', '.', 'o', ',', ';', ':', '?', '%', '!', '+', '-', '*', '(', ')', '<', '>', '_', '[', ']', '{', '}', '^', '°' ] 80 self.listeCaracteresPerso = [] 81 # Liste courante de caractères 82 self.listeCaracteres = self.listeCaracteresA 83 84 # Dictionnaire des caractères et de leur poids respectif 85 self.poidsNBCarac = {} 86 # Police utilisée pour la conversion (monospace) 87 if os.name == 'nt': 88 self.police = 'Lucida Console' 89 else: 90 self.police = 'Bitstream Vera Sans Mono' 91 # Nom de l'image à convertir. Par défaut, logo de ImageASCII 92 self.nomImage = "img/guttenberg.png" 93 # Nombre de caractères utilisés pour représenter l'image (tableau 2D nbrCarac x nbrCarac) 94 self.nbrCarac = 120.0 95 # Tolérance sur le poids. Détermine la marge d'erreur. 96 self.tolerancePoids = 0.05 97 # Précision de la tolérance de conversion. 98 self.precisionTolerance = 100.0 99 # Caractère par défaut, quand aucune correspondance n'est trouvée. 100 self.caractereDefaut = '-' 101 # Calculer le poids pour la liste courante de caractères. 102 self.calculerPoids()
103
104 - def calculerPoids (self, tailleCarac=20):
105 """ 106 Calculer le poids de chaque caractère, selon la liste de référence. 107 108 @keyword tailleCarac: Taille des caractères en pixels pour calculer le poids. 109 """ 110 self.poidsNBCarac = {} 111 for c in self.listeCaracteres: 112 poids = 0.000001 113 # Dessiner le caractère 'c' sur une image de 20x20 pixels 114 font = ImageFont.truetype(self.parent.listeFonts[self.police], tailleCarac) 115 img = Image.new('RGB', (20, 20), (255, 255, 255)) 116 carac = ImageDraw.Draw(img) 117 carac.text((0, 0), c, font=font, fill=(0, 0, 0) ) 118 # La méthode load renvoit un tableau en 2 dimensions comprennant les valeurs RVB de chaque pixel. 119 res = img.load() 120 121 # Parcourir les pixels et ajouter les valeurs 122 for y in range(20): 123 for x in range(20): 124 pix = res[x, y] 125 # Moyenne des valeurs sur les trois canaux. 126 p = ( pix[0]/255.0 + pix[1]/255.0 + pix[2]/255.0 ) / 3.0 127 # Additionner la valeur moyenne du pixel au poids total du caractère 128 poids += p 129 # Poids moyen pour le caractère. 130 poidsMoyen = float(poids) / float( img.size[0] * img.size[1] ) 131 # Mémoriser le résultat dans la liste des poids selon le caractère. 132 self.poidsNBCarac[c] = poidsMoyen 133 134 # Normaliser les poidsMoyen entre 0 et 1 135 # Il n'y a pas de caractère entièrement noir, le poids n'est donc pas une valeur comprise entre 0 et 1. 136 # On corrige les poids en fonction du plus petit et du plus grand poids qui servent respectivement 137 # de 0 et de 1. 138 pMin = 1.0 139 pMax = 0.00001 140 # Chercher le plus petit et le plus grand poids. 141 for c in self.poidsNBCarac: 142 if self.poidsNBCarac[c] < pMin: 143 pMin = self.poidsNBCarac[c] 144 if self.poidsNBCarac[c] > pMax: 145 pMax = self.poidsNBCarac[c] 146 # Modifier le poids de chaque caractère en le normalisant entre le poids minimum et maximum. 147 for c in self.poidsNBCarac: 148 self.poidsNBCarac[c] = ( self.poidsNBCarac[c]-pMin ) / ( pMax-pMin)
149 150
151 - def convertirImg_Txt (self, tailleCarac = 10):
152 """ 153 Convertir l'image en texte. 154 155 Charge une image et la découpe en damier. Pour chaque case, la 156 valeur moyenne des pixels est calculée. 157 158 @keyword tailleCarac: Taille des caractères en pixels. 159 """ 160 161 self.img = Image.open(self.nomImage) 162 self.img = self.img.convert("RGB") 163 164 # Image en 256 couleurs pour la sauvegarde en html et rtf (diminuer le nombre de couleurs) 165 self.img256 = self.img.convert("P") 166 167 tableCouleur = self.img256.resize((256, 1)) 168 tableCouleur.putdata(range(256)) 169 tableCouleur = tableCouleur.convert("RGB").getdata() 170 171 self.dimImg = Vecteur2D(self.img.size[0], self.img.size[1]) 172 # Redimentionner l'image si sa largeur est plus petite que le nombre de caractères 173 if self.dimImg.x < self.nbrCarac: 174 ratio = self.nbrCarac / self.dimImg.x 175 self.img = self.img.resize( ( int(self.dimImg.x*ratio), int(self.dimImg.y*ratio) ), Image.ANTIALIAS ) 176 self.dimImg = Vecteur2D(self.img.size[0], self.img.size[1]) 177 178 Ri = float(self.dimImg.x) / float(self.dimImg.y) 179 # Ratio de la police 180 # Multiplié par 2. Evite les ratios trop élevés pour les petites polices. 181 font = ImageFont.truetype( self.parent.listeFonts[self.police], tailleCarac*2) 182 tempo = Image.new('RGB', (20, 20), (255, 255, 255)) 183 car = ImageDraw.Draw(tempo) 184 car.text((0, 0), 'S', font=font, fill=(0, 0, 0) ) 185 dim = car.textsize('s', font=font) 186 ratioFont = float(dim[1]) / float(dim[0]) 187 188 i = self.dimImg.x/self.nbrCarac 189 self.vNbrCarac = Vecteur2D( self.nbrCarac, self.nbrCarac / Ri ) 190 self.vNbrCarac.y = self.vNbrCarac.y / ratioFont 191 j = self.dimImg.y / self.vNbrCarac.y 192 self.dimDamier = Vecteur2D( i, j ) 193 194 # Variables pour rendre compte de la progression de la conversion (jauge de progression). 195 progression = 0 196 pasProgression = 100.0 / (self.vNbrCarac.x * self.vNbrCarac.y) 197 198 self.vNbrCarac.x = int(self.vNbrCarac.x) 199 self.vNbrCarac.y = int(self.vNbrCarac.y) 200 # Poids noir/blanc de l'image pour chaque portion. 201 self.tabImg = [] 202 # Couleur moyenne pour chaque portion 203 self.tabCouleur = [] 204 # Idem mais en 256 couleurs (approximativement car moyenne des couleurs) 205 self.tabCouleur256 = [] 206 # Parcourir l'image 207 for ligne in range(self.vNbrCarac.y): 208 self.tabImg.append( [] ) 209 self.tabCouleur.append( [] ) 210 self.tabCouleur256.append( [] ) 211 for colonne in range(self.vNbrCarac.x): 212 # Mise à jour de la progression. 213 self.parent.jauge.SetValue( int(progression) ) 214 progression += pasProgression 215 # Lire la portion et calculer sa valeur 216 a = int(colonne*self.dimDamier.x) 217 b = int(ligne*self.dimDamier.y) 218 c = int(colonne*self.dimDamier.x+self.dimDamier.x) 219 d = int(ligne*self.dimDamier.y+self.dimDamier.y) 220 portion = self.img.crop( (a, b, c, d) ) 221 por = portion.load() 222 223 portion256 = self.img256.crop( (a, b, c, d) ) 224 por256 = portion256.load() 225 226 poids = 0.00001 227 poidsC = {'r': 0, 'v': 0, 'b': 0} 228 poidsC256 = {'r': 0, 'v': 0, 'b': 0} 229 # Parcourir chaque pixel de la portion. 230 for y in range(portion.size[1]): 231 for x in range(portion.size[0]): 232 pix = por[x, y] 233 pix256 = tableCouleur[por256[x, y]] 234 p = ( pix[0]/255.0 + pix[1]/255.0 + pix[2]/255.0 ) / 3.0 235 poids += p 236 237 poidsC = {'r': poidsC['r']+pix[0], 'v': poidsC['v']+pix[1], 'b': poidsC['b']+pix[2] } 238 poidsC256 = {'r': poidsC256['r']+pix256[0], 'v': poidsC256['v']+pix256[1], 'b': poidsC256['b']+pix256[2] } 239 240 nbrPixelMax = ((c-a) * (d-b)) 241 poidsMoyen = float(poids) / nbrPixelMax 242 self.tabImg[ligne].append( poidsMoyen ) 243 244 poidsMoyenC = {'r': poidsC['r'] / nbrPixelMax, 'v': poidsC['v'] / nbrPixelMax, 'b': poidsC['b'] / nbrPixelMax} 245 self.tabCouleur[ligne].append( poidsMoyenC ) 246 247 poidsMoyenC256 = {'r': poidsC256['r'] / nbrPixelMax, 'v': poidsC256['v'] / nbrPixelMax, 'b': poidsC256['b'] / nbrPixelMax} 248 self.tabCouleur256[ligne].append( poidsMoyenC256 ) 249 250 # Parcourir la table et remplacer par les caractères correspondants. 251 self.tabAscii = [] 252 ligne = 0 253 for y in self.tabImg: 254 self.tabAscii.append( [] ) 255 for x in y: 256 car = self.trouverCaractereProche ( self.poidsNBCarac, x, self.tolerancePoids ) 257 self.tabAscii[ligne].append( car ) 258 259 ligne += 1 260 # Ecrire le texte 261 txt = '' 262 for y in self.tabAscii: 263 for x in y: 264 txt += x 265 txt += '\n' 266 267 self.parent.jauge.SetValue( 100 ) 268 269 return txt
270 271
272 - def convertirTxtRtf(self, fichier, texte):
273 """ 274 Convertir un texte en rtf. 275 276 @param fichier: Nom et chemin de sauvegarde de l'image. 277 @param texte: Texte à convertir en image. 278 """ 279 280 tailleTxt = self.parent.tailleCarac.GetValue() * 2 281 # Créer un nouveau document 282 doc = Document() 283 ss = doc.StyleSheet 284 # Ajouter les fonts (Lucida et Courier sont déja reconnues) 285 f = Font( 'monofur', 'monofur' ) 286 ss.Fonts.append( f ) 287 f = Font( 'Bitstream Vera Sans Mono', 'Bitstream Vera Sans Mono' ) 288 ss.Fonts.append( f ) 289 f = Font( 'White Rabbit', 'White Rabbit' ) 290 ss.Fonts.append( f ) 291 f = Font( 'Crystal', 'Crystal' ) 292 ss.Fonts.append( f ) 293 f = Font( 'ModeNine', 'ModeNine' ) 294 ss.Fonts.append( f ) 295 296 self.fontsRTF = {'Monofur': ss.Fonts.monofur, 'Bitstream Vera Sans Mono': ss.Fonts.BitstreamVeraSansMono, 'White Rabbit': ss.Fonts.WhiteRabbit, 297 'Courier New': ss.Fonts.CourierNew, 'Crystal': ss.Fonts.Crystal, 'Lucida Console': ss.Fonts.LucidaConsole, 'ModeNine': ss.Fonts.ModeNine } 298 299 section = Section() 300 p = Paragraph() 301 302 if self.parent.teinteCarac.GetSelection() == 0: 303 304 lignes = texte.split("\n") 305 for ligne in lignes: 306 307 p.append( TEXT( str(ligne.encode('latin-1') ), LINE, font=self.fontsRTF[self.police], size=tailleTxt ) ) 308 309 else: 310 if self.parent.teinteCarac.GetSelection() == 1: 311 couleur = False 312 else: 313 couleur = True 314 315 nouvelleTable = [] 316 # Index du nombre minimum de couleurs 317 self.indexMin = [] 318 319 ii, jj = 0, 0 320 for j in self.tabCouleur256: 321 nouvelleTable.append( [] ) 322 for i in j: 323 # Créer une couleur à partir de la couleur moyenne de la portion 324 cCourante = Couleur(i['r'], i['v'], i['b']) 325 # Vrai si la couleur est déja indexée 326 existante = False 327 index = 0 328 for c in self.indexMin: 329 # Test si la couleur existe. Large tolérance pour diminuer le nombre de couleurs différentes 330 if c == cCourante: 331 existante = True 332 break 333 index += 1 334 # Si la couleur n'est pas encore indexée, on l'ajoute. 335 if not existante: 336 self.indexMin.append(cCourante) 337 # Attribuer à la nouvelle table l'index de couleur pour le caractère courant 338 nouvelleTable[jj].append( index ) 339 340 ii += 1 341 jj += 1 342 343 # Applatir le tableau de 256 couleurs (2D->1D) 344 tabCRTF = [] 345 for y in nouvelleTable: 346 for x in y: 347 tabCRTF.append(x) 348 tabCRTF.append(None) 349 # Créer les modèles de couleur pour le fichier rtf 350 listeCol = [] 351 i = 0 352 for s in range(len(self.indexMin)): 353 if couleur: 354 coul = Colour( str(s), self.indexMin[s].r, self.indexMin[s].v, self.indexMin[s].b) 355 else: 356 cg = int( (self.indexMin[s].r + self.indexMin[s].v + self.indexMin[s].b) / 3.0 ) 357 coul = Colour( str(s), cg, cg, cg) 358 ss.Colours.append(coul) 359 listeCol.append(coul) 360 i += 1 361 # Attribuer les couleurs, selon leur index, à chaque caractère 362 i = 0 363 for c in texte: 364 if c != '\n': 365 p.append( TEXT( str(c.encode('latin-1') ), font=self.fontsRTF[self.police], size=tailleTxt, colour=listeCol[ tabCRTF[i] ] ) ) 366 else: 367 p.append( LINE ) 368 i += 1 369 370 section.append(p) 371 doc.Sections.append( section ) 372 373 # Sauvegarder le fichier rtf 374 DR = Renderer() 375 DR.Write( doc, OpenFile( fichier[:-4] ) )
376
377 - def convertirTxtImg(self, fichier, texte, format):
378 """ 379 Convertir un texte en image. 380 381 @param fichier: Nom et chemin de sauvegarde de l'image. 382 @param texte: Texte à convertir en image. 383 @param format: Format de sauvegarde ('PNG', 'JPEG', etc.) 384 """ 385 # La taille des caractères est multipliée par 4 pour un meilleur rendu (une image finale plus grande) 386 taillePolice = self.parent.tailleCarac.GetValue() * 2 # * 4 387 texte = self.convertirImg_Txt(tailleCarac=taillePolice) 388 389 # Récupérer chaque ligne du texte. 390 lignes = texte.split('\n') 391 # Dessiner la première ligne 392 font = ImageFont.truetype( self.parent.listeFonts[self.police], taillePolice) 393 tempo = Image.new('RGB', (20, 20), (255, 255, 255)) 394 car = ImageDraw.Draw(tempo) 395 car.text((0, 0), lignes[0], font=font, fill=(0, 0, 0) ) 396 # Récupérer la taille de la première ligne (aussi longue que les suivantes). 397 dim = car.textsize(lignes[0], font=font) 398 # Dimensions de l'image finale. 399 dimX = dim[0] 400 dimY = dim[1] * len(lignes) 401 # Créer une image pour sauver le résultat. 402 resultat = Image.new('RGB', (dimX, dimY), (255, 255, 255)) 403 404 if self.parent.teinteCarac.GetSelection() == 0: 405 # Noir / blanc 406 ligneX = dim[0] 407 ligneY = dim[1] 408 posY = 0 409 for ligne in lignes: 410 # Dessiner la ligne courante. 411 img = Image.new('RGB', (ligneX, ligneY), (255, 255, 255)) 412 carac = ImageDraw.Draw(img) 413 carac.text((0, 0), ligne, font=font, fill=(0, 0, 0) ) 414 # Copier la ligne sur l'image de résultat. 415 resultat.paste( img, (0, posY) ) 416 # Passer à la ligne suivante. 417 posY += ligneY 418 else: 419 420 if self.parent.teinteCarac.GetSelection() == 1: 421 couleur = False 422 else: 423 couleur = True 424 425 tabC = [] 426 for y in self.tabCouleur: 427 for x in y: 428 tabC.append(x) 429 430 font = ImageFont.truetype( self.parent.listeFonts[self.police], taillePolice) 431 tempo = Image.new('RGB', (20, 20), (255, 255, 255)) 432 car = ImageDraw.Draw(tempo) 433 car.text((0, 0), lignes[0][0], font=font, fill=(0, 0, 0) ) 434 # Récupérer la taille de la première ligne (aussi longue que les suivantes). 435 dimCar = car.textsize(lignes[0][0], font=font) 436 437 # Couleur ou niveau de gris 438 ligneX = dim[0] 439 ligneY = dim[1] 440 posX = 0 441 posY = 0 442 i = 0 443 for ligne in lignes: 444 img = Image.new('RGB', (dimCar[0], dimCar[1]), (255, 255, 255)) 445 # Dessiner le caractère courant. 446 for c in ligne: 447 448 img = Image.new('RGB', (dimCar[0], dimCar[1]), (255, 255, 255)) 449 carac = ImageDraw.Draw(img) 450 if couleur: 451 carac.text((0, 0), c, font=font, fill=(tabC[i]['r'], tabC[i]['v'], tabC[i]['b']) ) 452 else: 453 cg = int( (tabC[i]['r'] + tabC[i]['v'] + tabC[i]['b']) / 3.0 ) 454 carac.text((0, 0), c, font=font, fill=(cg, cg, cg) ) 455 # Copier la ligne sur l'image de résultat. 456 resultat.paste( img, (posX, posY) ) 457 posX += dimCar[0] 458 459 i += 1 460 461 posY += dimCar[1] 462 posX = 0 463 464 465 # Sauver l'image 466 resultat.save( fichier, format)
467
468 - def trouverCaractereProche ( self, table, poids, tolerance ):
469 """ 470 Cherche le caractère le plus proche d'un poids donné. 471 472 @param table: Dictionnaire des caractères et de leurs poids respectifs {'a': 0.456, 'b': 0.563, ... } 473 @param poids: Poids dont on cherche la correspondance avec la table. 474 @param tolerance: Différence de poids acceptable entre le poids cherché et le poids testé. 475 @return: Le caractère le plus proche du poids entré en paramètre. 476 """ 477 for t in table: 478 if poids <= (table[t]+tolerance) and poids >= (table[t]-tolerance): 479 return t 480 # Si on ne trouve pas de correspondance, on retourne le caractère par défaut. 481 return self.caractereDefaut
482