Programarq: Funcionalidad Cad. Equidistancia en Python y GTK3

J'ai été en programmant il y a quelques annés. Maintenant, je suis en train de développer un projet dans lequel le logiciel gère une machine. La langue de cette dernière partie est Python 3. 
J'ai trouvé certaines des fonctionalités intéressantes, donc voilá.
 
Dans ce cas, c'est que étant donné les coordonnées d'une polyligne, on crée un nouveau poliligne pour un equidistance donné.
 
L'explication de cette équidistance ou de décalage (offset, ou inset appellé dans le logiciel 3D Blender) est: 
1 Tout d'abord, il est nécessaire de différencier les polylignes selon si elles sont fermées ou ouvertes, comme le moyen de trouver les points.:
  1. if polyline.coords[0] == polyline.coords[max_index]:
  2. ...
  3. else:
  4. ...
2. Dans le premier cas, la poliligne est fermée.
2a. On trouve les deux vecteurs que correspondent aux arêtes précédent et subséquent au point.
2b. On trouve l'angle bissecteur parmi les deux vecteurs.
2c. On trouve la distance pour se deplacer selon le bissecteur. Cette distance será trouvée par trigonométrie selon:
2d. On multiplie cette derniere distance par le versor (vecteur unitaire) du bissecteur et on arrive à un nouveau bissecteur:
  1. vector_pos = Vector (self.coords[x], self.coords[x+1]).get_versor()
  2. vector_pre = Vector (self.coords[x], self.coords[x-2]).get_versor()
  3. bisector_angle = (vector_pos.get_angle_vectors(vector_pre)) / 2
  4. dist_bisector = offset_dist / math.sin(math.radians(bisector_angle))
  5. bisector = (vector_pos + vector_pre).get_versor() * dist_bisector
2e. On somme le nouveau bissecteur au point et le résultat  est le nouveau point équidistante:
new_point = point_x.plus_vector(bisector)
2f. Il y a deux particularités:
a. Le premier point (vertex) doit être consideré le avant-dernier point de la liste de coordonnées, puisque le dernier est le même que le premier et ce donnerá un vecteur nul.
b. Le dernier point est dejá calculé et il est le même que le premier point equidistante.
 
3. Dans deuxième cas, la poli ligne est fermée. Ça veut dire qu'il n'y a pas de arêtes juste à côté des premier et dernier points. Dans ce cas-là, le point équidistante sera calculé selon la perpendiculaire au vecteur qui relie le point avec le suivant/précédent .
3b. Pour obtenir le premier point équidistante, on estime que le vecteur perpendiculaire et le vecteur de la bissectrice de la  suivante/précédente arête ne différent pas de plus de 90 °, dans ce cas, le premier est inversé.
3b. Pour obtenir le dernier point équidistant, il faut assurer que le vecteur perpendiculaire est parallel au vecteur de l'arête précédente. Si le résultat est négatif, le vecteur sera inversé.
 
Ici, je poste le code qui charge une fenêtre pour entrer les coordonnées en mode liste et dessiner une poliligne. Ensuite, l'introduction de la distance de décalage (positif ou négatif). Sous l'archive avec le code, l'interface graphique et la feuille de style à la fin de la page. Pour travailler, en plus de python 3 installé est également nécessaire PyGObject.
  1. <span style="line-height: 20.0063037872314px;">
  2. '''
  3. Created on 17/10/2014
  4.  
  5. @author: fer
  6. http://www.asimply.com
  7. '''
  8.  
  9. import sys
  10. import os
  11.  
  12. from gi.repository import Gtk, GdkPixbuf, Gdk
  13. import cairo
  14. import math
  15.  
  16. # Variable for decimal accurancy
  17. precision = 4
  18.  
  19. # Style for the GUI
  20. screen = Gdk.Screen.get_default()
  21. css_provider = Gtk.CssProvider()
  22. css_provider.load_from_path('style.css')
  23. context = Gtk.StyleContext()
  24. context.add_provider_for_screen(screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
  25.  
  26. # Canvas class
  27. class Canvas(Gtk.DrawingArea):
  28. # The canvas
  29. def __init__(self):
  30. Gtk.DrawingArea.__init__(self)
  31.  
  32. # Create buffer
  33. self.double_buffer = None
  34. self.scale = 1
  35.  
  36. self.connect("draw", self.on_draw)
  37. self.connect("configure-event", self.on_configure_event)
  38.  
  39. self.show()
  40.  
  41.  
  42. def on_draw(self, widget, cr):
  43. #Throw double buffer into widget drawable
  44.  
  45. if self.double_buffer is not None:
  46. cr.set_source_surface(self.double_buffer, 0.0, 0.0)
  47. cr.paint()
  48. else:
  49. print('Invalid double buffer')
  50.  
  51. return False
  52.  
  53. def on_configure_event(self, widget, event, data=None):
  54. self.on_configure()
  55.  
  56. def on_configure(self):
  57. # Destroy previous buffer
  58. if self.double_buffer is not None:
  59. self.double_buffer.finish()
  60. self.double_buffer = None
  61.  
  62. # Create a new buffer
  63. self.double_buffer = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.get_allocated_width(), self.get_allocated_height())
  64.  
  65. db = self.double_buffer
  66. if db is not None:
  67. # Create cairo context with double buffer as is DESTINATION
  68. self.cairo_ctx = cairo.Context(db)
  69.  
  70. else:
  71. print('Invalid double buffer')
  72.  
  73. return False
  74.  
  75.  
  76. # Deletes the buffer and the context
  77. def empty_canvas(self):
  78. #Configure the double buffer based on size of the widget
  79. # Destroy previous buffer
  80. if self.double_buffer is not None:
  81. self.double_buffer.finish()
  82. self.double_buffer = None
  83. self.cairo_ctx = None
  84.  
  85. #self.queue_draw()
  86. return
  87.  
  88. # Draws a polyline in the canvas from a coordinate list
  89. def draw_polyline(self, coordinates_list, line_width = 1, color_line = (0,1,1)):
  90.  
  91. for coordinate in coordinates_list:
  92. self.cairo_ctx.line_to(coordinate[0], coordinate[1])
  93.  
  94. self.cairo_ctx.set_source_rgb(color_line[0], color_line[1], color_line[2])
  95. self.cairo_ctx.set_line_width(line_width)
  96.  
  97. self.cairo_ctx.stroke()
  98.  
  99. self.double_buffer.flush()
  100.  
  101. def draw_space (self, entity):
  102. #if entity.has_child() and (entity.type == "Space" or entity.type == "Polyline"):
  103. if entity.type == "Space" or entity.type == "Polyline":
  104. for entity_inside in entity.list_entities:
  105. if entity_inside.type == "Polyline":
  106. self.draw_polyline(entity_inside.coords)
  107. if entity_inside.has_child():
  108. self.draw_space(entity_inside)
  109.  
  110. def OnDraw(self, widget, cr):
  111.  
  112. if self.double_buffer is not None:
  113. cr.set_source_surface(self.double_buffer, 0.0, 0.0)
  114. cr.paint()
  115. else:
  116. print('Invalid double buffer')
  117.  
  118. return False
  119.  
  120.  
  121. # Main GUI
  122. class WindowGui(Gtk.ApplicationWindow):
  123. # a window
  124. def __init__(self, app):
  125. Gtk.Window.__init__(self, title="StatusBar", application=app)
  126. #self.set_default_size(200, 100)
  127.  
  128. self.gladefile = "canvas_offset_web.glade"
  129. self.builder = Gtk.Builder()
  130. self.builder.add_from_file(self.gladefile)
  131. self.builder.connect_signals(self)
  132. #self.builder.connect_signals({ "on_window_destroy" : Gtk.main_quit })
  133. self.window = self.builder.get_object("applicationwindow1")
  134. self.frame_canvas = self.builder.get_object("box1_canvas")
  135. self.canvas = Canvas()
  136. self.frame_canvas.pack_start(self.canvas, True, True, 0)
  137.  
  138. self.window.show()
  139.  
  140. """
  141. def on_draw(self, widget):
  142. self.canvas.draw_space(new_space)
  143. self.canvas.queue_draw()
  144. """
  145.  
  146. def getLabelValue(self, labelGui):
  147. self.label = self.builder.get_object(labelGui)
  148. return self.label.get_text()
  149.  
  150. def putMessageLabel(self, labelGui, message):
  151. self.label = self.builder.get_object(labelGui)
  152. self.label.set_text(str(message))
  153.  
  154. def get_values_in_list(self, string):
  155. coords = []
  156. for i in string:
  157.  
  158. if i == "[" or i == "(":
  159. coord = []
  160. elif i == "]" or i == ")":
  161. coord = []
  162. elif i == 0:
  163. pass
  164.  
  165. coord.append
  166.  
  167. list.append(int(string[i]))
  168.  
  169. def on_draw(self, widget):
  170. rect_coords = [[10,10,0], [50,10,0], [50,50,0], [10,50,0] , [10,10,0]] # [10,10,0], [50,10,0], [50,50,0], [10,50,0] , [10,10,0]
  171. #polyline2 = new_space.create_entity("Polyline", rect_coords_open)
  172.  
  173. self.space = Space()
  174.  
  175. try:
  176. value = self.getLabelValue("coords_list")
  177. # Deletes the first "[" and last "]" with: value[1:len(value)-1]
  178. #value = value[0:len(value)]
  179. # Removes whitespaces
  180. value = value.replace(' ', '')
  181. # Get the string between '],['
  182. list = [s for s in (value[1:len(value)-1]).split('],[')]
  183. coords = []
  184. for item in list:
  185. coord = [int (s) for s in item.split(",")]
  186. coords.append (coord)
  187. self.space.create_entity("Polyline", coords)
  188. self.canvas.draw_space(self.space)
  189. self.canvas.queue_draw()
  190. except:
  191. error_message = "Incorrect coordinates list: " + str(value)
  192. print (error_message)
  193. self.putMessageLabel("coords_list", error_message)
  194. return
  195.  
  196.  
  197. def on_draw_clear(self, widget):
  198. self.canvas.on_configure()
  199. self.canvas.queue_draw()
  200.  
  201. def on_offset(self, widget):
  202. try:
  203. offset_distance = float (self.getLabelValue("offset_dist"))
  204. for polyline in self.space.list_entities:
  205. new_polyline = self.space.get_offset(polyline, offset_distance)
  206. self.canvas.draw_polyline(new_polyline.coords, line_width = 1, color_line= (1,0,0))
  207. self.canvas.queue_draw()
  208. except:
  209. error_message = "Incorrect offset distance: " + str(self.getLabelValue("offset_dist"))
  210. print (error_message)
  211. self.putMessageLabel("offset_dist", error_message)
  212.  
  213.  
  214. # Main Class with the basic entity properties
  215. class Entity (object):
  216. def __init__(self):
  217. self.type = ""
  218. self.list_entities = []
  219.  
  220. def has_child(self):
  221. if self.list_entities == []:
  222. return False
  223. else:
  224. return True
  225.  
  226. def create_entity(self, type, *args):
  227. if type == "Point" or type == "point":
  228. new_entity = Point (*args) # args[0], args[1])
  229. elif type == "Vector":
  230. new_entity = Vector (args[0], args[1])
  231. elif type == "Polyline":
  232. new_entity = Polyline (args[0])
  233. else:
  234. print ("Unknown type")
  235. return
  236. self.list_entities.append(new_entity)
  237. return new_entity
  238.  
  239. def print_all_entities(self):
  240. for entity in self.list_entities:
  241. print (entity.type, " ", entity, "Has child?: ", entity.has_child())
  242. if entity.has_child():
  243. entity.print_all_entities()
  244.  
  245.  
  246. # Point Class
  247. class Point (Entity):
  248. def __init__(self, x = 0, y = 0, z = 0):
  249. Entity.__init__(self)
  250. self.type = "Point"
  251. self.x = x
  252. self.y = y
  253. self.z = z
  254. self.coords = [x, y, z]
  255.  
  256. def __str__(self):
  257. return str(self.coords)
  258.  
  259. def __add__(self, point):
  260. return Point(self.x + point.x, self.y + point.y) #, self.z + point.z)
  261.  
  262. def __sub__(self, point):
  263. return Point(self.x-point.x, self.y-point.y) #, self.z-point.z)
  264.  
  265. # Vector Class
  266. class Vector(Entity):
  267. def __init__(self, value1, value2):
  268. Entity.__init__(self)
  269. self.type = "Vector"
  270. #### Two points are given
  271. if type(value1).__name__ == "Point" and type(value2).__name__ == "Point":
  272. self.x = value2.x - value1.x
  273. self.y = value2.y - value1.y
  274. #### Two list of coordinates are given
  275. elif isinstance(value1, list) and isinstance(value2, list):
  276. self.x = value2[0] - value1[0]
  277. self.y = value2[1] - value1[1]
  278. #### Two coordinates are given
  279. elif (isinstance(value1, float) or isinstance(value1, int)) and (isinstance(value2, float) or isinstance(value2, int)):
  280. self.x = value1
  281. self.y = value2
  282. #### The given parameters aren't correct
  283. else:
  284. print ("The given parameters don't fit with requirements. Vector not created.")
  285. return None
  286.  
  287. self.get_module()
  288.  
  289. def __str__(self):
  290. return str([self.x, self.y])
  291.  
  292. def __add__(self, vector):
  293. return Vector(self.x+vector.x, self.y+vector.y)
  294.  
  295. def __sub__(self, vector):
  296. return Vector(self.x-vector.x, self.y-vector.y)
  297.  
  298. def __mul__(self, scalar):
  299. return Vector(self.x*scalar, self.y*scalar)
  300.  
  301. # Producto escalar, interno o punto entre vectores
  302. def mul_point (self, vector):
  303. return self.x * vector.x + self.y * vector.y
  304.  
  305. def get_slope (self):
  306. if self.x == 0 and self.y > 0:
  307. self.slope = float("inf")
  308. elif self.x == 0 and self.y < 0:
  309. self.slope = float("-inf")
  310. else:
  311. self.slope = round((self.y / self.x), precision)
  312. return self.slope
  313.  
  314. def get_from_coordinates(self, x1, y1, x2, y2):
  315. self.x = x2 - x1
  316. self.y = y2 - y1
  317.  
  318. def get_from_two_points(self, point1, point2):
  319. self.x = point2[0] - point1[0]
  320. self.y = point2[1] - point1[1]
  321.  
  322. def get_module (self):
  323. self.module = math.sqrt(self.x**2 + self.y**2)
  324.  
  325. def get_versor (self): # vector with the same direction but module equals to 1
  326. if self.module != 0:
  327. vx = self.x / self.module
  328. vy = self.y / self.module
  329. return Vector(vx, vy)
  330. else:
  331. print ("The module of the vector is equals to zero")
  332. return None
  333.  
  334. def get_perpendicular_vector(self):
  335. x = -self.y
  336. y = self.x
  337. perp_vector = Vector(x, y)
  338. return perp_vector
  339.  
  340. def get_reversed (self):
  341. return Vector(-self.x, -self.y)
  342.  
  343. # Get distance from vector to point
  344. def get_distance_to_point(self, point):
  345. # No es correcto, no funciona para vectores libres.
  346. vector_3 = Vector(Point (self.x, self.y))
  347. x_scalar = self.mul_point(vector_3)
  348. vector_proj = Vector(self.get_versor() * x_scalar)
  349. # Da el vector paralelo de dimensión la proyección
  350. return vector_proj
  351.  
  352. def get_angle_vectors (self, vector):
  353. cos_angle = self.mul_point(vector) / (self.module * vector.module)
  354. return math.degrees(math.acos(cos_angle))
  355.  
  356. class Polyline (Entity):
  357. def __init__(self, coords = []):
  358. Entity.__init__(self)
  359. self.type = "Polyline"
  360.  
  361. if coords == []:
  362. self.coords = coords
  363. return
  364. else:
  365. #### A list of Points are given
  366. if type(coords[0]).__name__ == "Point":
  367. self.coords = []
  368. for point in coords:
  369. self.list_entities.append(point)
  370. self.coords.append(point.coords)
  371. #### A list of coords are given
  372. elif isinstance(coords[0], list):
  373. for coord in coords:
  374. self.create_entity("Point", coord[0], coords[1], coords[2])
  375. self.coords = coords
  376. #### The given parameters aren't correct
  377. else:
  378. print ("The given parameters don't fit with requirements. Polyline not created.")
  379. return None
  380.  
  381. self.coords = coords
  382.  
  383. def add_point_to_coords (self, point):
  384. self.coords.append(point.coords)
  385.  
  386. def coords_update(self):
  387. self.coords = []
  388. for point in self.list_entities:
  389. self.coords.append(point.coords)
  390.  
  391.  
  392. # The Class for the entity Space where all the entities has sense
  393. class Space (Entity):
  394. def __init__(self):
  395. Entity.__init__(self)
  396. self.type = "Space"
  397. self.list_points = []
  398. self.list_vectors = []
  399. self.list_lines = []
  400. self.list_polylines = []
  401. self.entities = []
  402.  
  403. def get_perpendicular_vector(self, vector):
  404. x = -vector.y
  405. y = vector.x
  406. perp_vector = Vector(x, y)
  407. return perp_vector
  408.  
  409. def get_point_plus_vector (self, point, vector):
  410. newpoint_x = point.x + vector.x
  411. newpoint_y = point.y + vector.y
  412. return Point (newpoint_x, newpoint_x)
  413.  
  414. def dist_point_line(self, point, line):
  415. if line.slope == float("inf"):
  416. print ("recta paralela a ordenadas")
  417. distance = point.x
  418. else:
  419. distance = ((line.slope * point.x) - point.y + line.y_intersection) / (((line.slope)**2+1)**(1/2))
  420. return distance
  421.  
  422. ##HALLAR LA INTERSECCION ENTRE RECTAS
  423. def lines_intersection (self, line1, line2):
  424. delta_slope = line1.slope - line2.slope
  425. #if (line1.slope - line2.slope == 0 and line1.slope != line2.slope and line1.ecuation[1]==line2.ecuation[1]):
  426. if (delta_slope == 0 or delta_slope == "nan" and line1.ecuation[1]==line2.ecuation[1]):
  427. print ("Son la misma recta o paralelas, no se cruzan")
  428. return "Same"
  429. elif (delta_slope == 0 or delta_slope == "nan" and line1.ecuation[1]!=line2.ecuation[1]):
  430. print ("Son paralelas, no se cruzan")
  431. return "Parallel"
  432. elif (line1.slope == float ('inf')): ##La primera recta es paralela al eje y
  433. xinterseccion = float(line1.point1.x)
  434. yinterseccion = line2.slope*xinterseccion + line2.ecuation[1]
  435. return Point(round(xinterseccion, precision), round(yinterseccion, precision))
  436. elif (line2.slope == float ('inf')): ##La segunda recta es paralela al eje y
  437. xinterseccion = float(line1.point2.x)
  438. yinterseccion = line1.slope*xinterseccion + line1.ecuation[1]
  439. return Point(round(xinterseccion, precision), round(yinterseccion, precision))
  440. else:
  441. xinterseccion = (line2.ecuation[1]-line1.ecuation[1])/(line1.slope - line2.slope)
  442. yinterseccion = line1.slope*xinterseccion + line1.ecuation[1]
  443. return Point(round(xinterseccion, precision), round(yinterseccion,precision))
  444.  
  445. ## Hallar la bisectriz de dos lineas
  446. def get_bisector(self, line1, line2):
  447. intersection_point = self.lines_intersection(line1, line2)
  448. if intersection_point == "Same":
  449. intersection_point = [self.point1, self.point2]
  450. elif intersection_point == "Parallel":
  451. pass
  452. #get_distance_between_lines
  453. else:
  454. # There must be an intersection
  455. pass
  456.  
  457. angle1 = math.degrees(math.atan(line1.slope))
  458. angle2 = math.degrees(math.atan(line2.slope))
  459. bisector_angle = (angle1 + angle2) / 2
  460. bisector_slope = math.tan(math.radians(bisector_angle))
  461.  
  462. return self.create_entity("Line", intersection_point, self.get_bisector_vector(line1.vector, line2.vector))
  463.  
  464. def get_bisector_vector (self, vector1, vector2):
  465. vector_suma = vector1 + vector2
  466. vector_bisector = vector_suma.get_versor()
  467. return vector_bisector
  468.  
  469. def get_offset(self, polyline, offset_dist):
  470. coords_amount = len(polyline.coords)
  471. max_index = coords_amount-1
  472.  
  473. list_new_points = []
  474. # Check if the polyline is closed (first check) or opened (second check)
  475. # If the polyline is closed:
  476. if polyline.coords[0] == polyline.coords[max_index]:
  477. for x in range(0, coords_amount):
  478. point_x = Point(polyline.coords[x][0], polyline.coords[x][1])
  479. if x == 0:
  480. vector_pos = Vector (polyline.coords[x], polyline.coords[x+1])
  481. vector_pre = Vector (polyline.coords[x], polyline.coords[x-2]) # This is beacuse it is a closed polyline
  482. bisector_angle = vector_pos.get_angle_vectors(vector_pre) / 2
  483. dist_bisector = math.sin(math.radians(bisector_angle)) * offset_dist * 2
  484. bisector = self.get_bisector_vector(vector_pos, vector_pre) * dist_bisector
  485. elif x == max_index:
  486. list_new_points.append(list_new_points[0])
  487. break
  488. else:
  489. vector_pos = Vector (polyline.coords[x], polyline.coords[x+1])
  490. vector_pre = Vector (polyline.coords[x], polyline.coords[x-1])
  491. bisector_angle = vector_pos.get_angle_vectors(vector_pre) / 2
  492. dist_bisector = math.sin(math.radians(bisector_angle)) * offset_dist * 2
  493. bisector = self.get_bisector_vector(vector_pos, vector_pre) * dist_bisector
  494. new_point = Point (*((point_x + bisector).coords))
  495. list_new_points.append(new_point)
  496.  
  497. # Polyline is opened
  498. else:
  499. sign = offset_dist/abs (offset_dist)
  500. offset_dist = abs (offset_dist)
  501. for x in range(0, coords_amount):
  502. point_x = Point(polyline.coords[x][0], polyline.coords[x][1])
  503. if x == 0:
  504. vector_pos = Vector (polyline.coords[x], polyline.coords[x+1])
  505. vector_perp = vector_pos.get_perpendicular_vector()
  506. vector_perp = vector_perp.get_versor()
  507. vector_pre_next = Vector (polyline.coords[x+1], polyline.coords[x])
  508. vector_pos_next = Vector (polyline.coords[x+1], polyline.coords[x+2])
  509. next_bisector = vector_pre_next + vector_pos_next
  510. test_angle = vector_perp.get_angle_vectors(next_bisector) * sign
  511. if test_angle > 0 and test_angle < 90:
  512. bisector = vector_perp * offset_dist
  513. else:
  514. bisector = vector_perp.get_reversed() * offset_dist
  515. elif x == max_index:
  516. vector_pre = Vector (polyline.coords[x], polyline.coords[x-1])
  517. vector_perp = vector_pre.get_perpendicular_vector()
  518. vector_perp = vector_perp.get_versor()
  519. test_angle = vector_perp.get_angle_vectors(next_bisector) * sign
  520. bisector = vector_perp * offset_dist
  521. vector_pre_offset = Vector ((point_x + bisector), new_point) # new_point here is the previous and last new_point created.
  522. angle_pre_vectors = vector_pre.get_angle_vectors(vector_pre_offset)
  523. # Checks if the two pre_vectors are parallel or not
  524. if angle_pre_vectors != 0:
  525. bisector = vector_perp.get_reversed() * offset_dist
  526. else:
  527. vector_pos = Vector (polyline.coords[x], polyline.coords[x+1])
  528. vector_pre = Vector (polyline.coords[x], polyline.coords[x-1])
  529. bisector_angle = vector_pos.get_angle_vectors(vector_pre) / 2
  530. dist_bisector = math.sin(math.radians(bisector_angle)) * offset_dist * 2
  531. bisector = self.get_bisector_vector(vector_pos, vector_pre) * dist_bisector * sign
  532. new_point = Point (*((point_x + bisector).coords))
  533. list_new_points.append(new_point)
  534. new_polyline = polyline.create_entity("Polyline", list_new_points) # The type could be the same of origin type
  535. new_polyline.coords_update()
  536.  
  537. return new_polyline
  538.  
  539.  
  540. # Class for running the app
  541. class MyApplication(Gtk.Application):
  542. def __init__(self):
  543. Gtk.Application.__init__(self)
  544.  
  545. def do_activate(self):
  546. self.win = WindowGui(self)
  547. #win.show_all()
  548.  
  549. def do_startup(self):
  550. Gtk.Application.do_startup(self)
  551.  
  552. if __name__ == "__main__":
  553. app = MyApplication()
  554. app.run(sys.argv)</span><font face="Lucida Grande, Lucida Sans Unicode, sans-serif"><span style="font-size: 11px; line-height: 16.2651252746582px; white-space: normal;">
  555. </span></font>
 
archivo: 
Fichier attachéTaille
Binary Data canvas_offset.tar_.gz6.08 Ko
Package icon canvas_offset.zip6.1 Ko