1 /*
2 Copyright (C) 2011 Elvis Stansvik <elvstone@gmail.com>
3 
4 For general Scribus (>=1.3.2) copyright and licensing information please refer
5 to the COPYING file provided with the program. Following this notice may exist
6 a copyright and/or license notice that predates the release of Scribus 1.3.2
7 for which a new license (GPL+exception) is in place.
8 */
9 
10 #include <QPointF>
11 
12 #include "tableborder.h"
13 
14 #include "tableutils.h"
15 #include "pageitem_table.h"
16 
17 namespace TableUtils
18 {
19 
resolveBordersHorizontal(const TableCell & topLeftCell,const TableCell & topCell,const TableCell & topRightCell,const TableCell & bottomLeftCell,const TableCell & bottomCell,const TableCell & bottomRightCell,TableBorder * topLeft,TableBorder * left,TableBorder * bottomLeft,TableBorder * center,TableBorder * topRight,TableBorder * right,TableBorder * bottomRight,PageItem_Table * table)20 void resolveBordersHorizontal(const TableCell& topLeftCell, const TableCell& topCell,
21 	const TableCell& topRightCell, const TableCell& bottomLeftCell, const TableCell& bottomCell,
22 	const TableCell& bottomRightCell, TableBorder* topLeft, TableBorder* left, TableBorder* bottomLeft,
23 	TableBorder* center, TableBorder* topRight, TableBorder* right, TableBorder* bottomRight, PageItem_Table* table)
24 {
25 	// Resolve top left.
26 	if (!topCell.isValid() && !bottomCell.isValid())
27 		return;
28 	if (topLeftCell.column() == topCell.column())
29 		*topLeft = TableBorder();
30 	else if (topLeftCell.isValid() && topCell.isValid())
31 		*topLeft = collapseBorders(topCell.leftBorder(), topLeftCell.rightBorder());
32 	else if (topLeftCell.isValid())
33 		*topLeft = collapseBorders(table->rightBorder(), topLeftCell.rightBorder());
34 	else if (topCell.isValid())
35 		*topLeft = collapseBorders(topCell.leftBorder(), table->leftBorder());
36 	else
37 		*topLeft = TableBorder();
38 	// Resolve left.
39 	if (topLeftCell.row() == bottomLeftCell.row())
40 		*left = TableBorder();
41 	else if (topLeftCell.isValid() && bottomLeftCell.isValid())
42 		*left = collapseBorders(bottomLeftCell.topBorder(), topLeftCell.bottomBorder());
43 	else if (topLeftCell.isValid())
44 		*left = collapseBorders(table->bottomBorder(), topLeftCell.bottomBorder());
45 	else if (bottomLeftCell.isValid())
46 		*left = collapseBorders(bottomLeftCell.topBorder(), table->topBorder());
47 	else
48 		*left = TableBorder();
49 	// Resolve bottom left.
50 	if (bottomLeftCell.column() == bottomCell.column())
51 		*bottomLeft = TableBorder();
52 	else if (bottomLeftCell.isValid() && bottomCell.isValid())
53 		*bottomLeft = collapseBorders(bottomCell.leftBorder(), bottomLeftCell.rightBorder());
54 	else if (bottomLeftCell.isValid())
55 		*bottomLeft = collapseBorders(table->rightBorder(), bottomLeftCell.rightBorder());
56 	else if (bottomCell.isValid())
57 		*bottomLeft = collapseBorders(bottomCell.leftBorder(), table->leftBorder());
58 	else
59 		*bottomLeft = TableBorder();
60 	// Resolve center.
61 	if (topCell.row() == bottomCell.row())
62 		*center = TableBorder();
63 	else if (topCell.isValid() && bottomCell.isValid())
64 		*center = collapseBorders(topCell.bottomBorder(), bottomCell.topBorder());
65 	else if (topCell.isValid())
66 		*center = collapseBorders(table->bottomBorder(), topCell.bottomBorder());
67 	else if (bottomCell.isValid())
68 		*center = collapseBorders(bottomCell.topBorder(), table->topBorder());
69 	else
70 		*center = TableBorder();
71 	// Resolve top right.
72 	if (topRightCell.column() == topCell.column())
73 		*topRight = TableBorder();
74 	else if (topRightCell.isValid() && topCell.isValid())
75 		*topRight = collapseBorders(topRightCell.leftBorder(), topCell.rightBorder());
76 	else if (topRightCell.isValid())
77 		*topRight = collapseBorders(topRightCell.leftBorder(), table->leftBorder());
78 	else if (topCell.isValid())
79 		*topRight = collapseBorders(table->rightBorder(), topCell.rightBorder());
80 	else
81 		*topRight = TableBorder();
82 	// Resolve right.
83 	if (topRightCell.row() == bottomRightCell.row())
84 		*right = TableBorder();
85 	else if (topRightCell.isValid() && bottomRightCell.isValid())
86 		*right = collapseBorders(bottomRightCell.topBorder(), topRightCell.bottomBorder());
87 	else if (topRightCell.isValid())
88 		*right = collapseBorders(table->bottomBorder(), topRightCell.bottomBorder());
89 	else if (bottomRightCell.isValid())
90 		*right = collapseBorders(bottomRightCell.topBorder(), table->topBorder());
91 	else
92 		*right = TableBorder();
93 	// Resolve bottom right.
94 	if (bottomRightCell.column() == bottomCell.column())
95 		*bottomRight = TableBorder();
96 	else if (bottomRightCell.isValid() && bottomCell.isValid())
97 		*bottomRight = collapseBorders(bottomRightCell.leftBorder(), bottomCell.rightBorder());
98 	else if (bottomRightCell.isValid())
99 		*bottomRight = collapseBorders(bottomRightCell.leftBorder(), table->leftBorder());
100 	else if (bottomCell.isValid())
101 		*bottomRight = collapseBorders(table->rightBorder(), bottomCell.rightBorder());
102 	else
103 		*bottomRight = TableBorder();
104 }
105 
resolveBordersVertical(const TableCell & topLeftCell,const TableCell & topRightCell,const TableCell & leftCell,const TableCell & rightCell,const TableCell & bottomLeftCell,const TableCell & bottomRightCell,TableBorder * topLeft,TableBorder * top,TableBorder * topRight,TableBorder * center,TableBorder * bottomLeft,TableBorder * bottom,TableBorder * bottomRight,PageItem_Table * table)106 void resolveBordersVertical(const TableCell& topLeftCell, const TableCell& topRightCell, const TableCell& leftCell, const TableCell& rightCell, const TableCell& bottomLeftCell,
107 	const TableCell& bottomRightCell, TableBorder* topLeft, TableBorder* top, TableBorder* topRight, TableBorder* center, TableBorder* bottomLeft, TableBorder* bottom, TableBorder* bottomRight, PageItem_Table* table)
108 {
109 	if (!leftCell.isValid() && !rightCell.isValid())
110 		return;
111 	// Resolve top left.
112 	if (topLeftCell.row() == leftCell.row())
113 		*topLeft = TableBorder();
114 	else if (topLeftCell.isValid() && leftCell.isValid())
115 		*topLeft = collapseBorders(leftCell.topBorder(), topLeftCell.bottomBorder());
116 	else if (topLeftCell.isValid())
117 		*topLeft = collapseBorders(table->bottomBorder(), topLeftCell.bottomBorder());
118 	else if (leftCell.isValid())
119 		*topLeft = collapseBorders(leftCell.topBorder(), table->topBorder());
120 	else
121 		*topLeft = TableBorder();
122 	// Resolve top.
123 	if (topLeftCell.column() == topRightCell.column())
124 		*top = TableBorder();
125 	else if (topLeftCell.isValid() && topRightCell.isValid())
126 		*top = collapseBorders(topRightCell.leftBorder(), topLeftCell.rightBorder());
127 	else if (topLeftCell.isValid())
128 		*top = collapseBorders(table->rightBorder(), topLeftCell.rightBorder());
129 	else if (topRightCell.isValid())
130 		*top = collapseBorders(topRightCell.leftBorder(), table->leftBorder());
131 	else
132 		*top = TableBorder();
133 	// Resolve top right.
134 	if (topRightCell.row() == rightCell.row())
135 		*topRight = TableBorder();
136 	else if (topRightCell.isValid() && rightCell.isValid())
137 		*topRight = collapseBorders(rightCell.topBorder(), topRightCell.bottomBorder());
138 	else if (topRightCell.isValid())
139 		*topRight = collapseBorders(table->bottomBorder(), topRightCell.bottomBorder());
140 	else if (rightCell.isValid())
141 		*topRight = collapseBorders(rightCell.topBorder(), table->topBorder());
142 	else
143 		*topRight = TableBorder();
144 	// Resolve center.
145 	if (leftCell.column() == rightCell.column())
146 		*center = TableBorder();
147 	else if (leftCell.isValid() && rightCell.isValid())
148 		*center = collapseBorders(rightCell.leftBorder(), leftCell.rightBorder());
149 	else if (leftCell.isValid())
150 		*center = collapseBorders(table->rightBorder(), leftCell.rightBorder());
151 	else if (rightCell.isValid())
152 		*center = collapseBorders(rightCell.leftBorder(), table->leftBorder());
153 	else
154 		*center = TableBorder();
155 	// Resolve bottom left.
156 	if (bottomLeftCell.row() == leftCell.row())
157 		*bottomLeft = TableBorder();
158 	else if (bottomLeftCell.isValid() && leftCell.isValid())
159 		*bottomLeft = collapseBorders(bottomLeftCell.topBorder(), leftCell.bottomBorder());
160 	else if (bottomLeftCell.isValid())
161 		*bottomLeft = collapseBorders(bottomLeftCell.topBorder(), table->topBorder());
162 	else if (leftCell.isValid())
163 		*bottomLeft = collapseBorders(table->bottomBorder(), leftCell.bottomBorder());
164 	else
165 		*bottomLeft = TableBorder();
166 	// Resolve bottom.
167 	if (bottomLeftCell.column() == bottomRightCell.column())
168 		*bottom = TableBorder();
169 	else if (bottomLeftCell.isValid() && bottomRightCell.isValid())
170 		*bottom = collapseBorders(bottomRightCell.leftBorder(), bottomLeftCell.rightBorder());
171 	else if (bottomLeftCell.isValid())
172 		*bottom = collapseBorders(table->rightBorder(), bottomLeftCell.rightBorder());
173 	else if (bottomRightCell.isValid())
174 		*bottom = collapseBorders(bottomRightCell.leftBorder(), table->leftBorder());
175 	else
176 		*bottom = TableBorder();
177 	// Resolve bottom right.
178 	if (bottomRightCell.row() == rightCell.row())
179 		*bottomRight = TableBorder();
180 	else if (bottomRightCell.isValid() && rightCell.isValid())
181 		*bottomRight = collapseBorders(bottomRightCell.topBorder(), rightCell.bottomBorder());
182 	else if (bottomRightCell.isValid())
183 		*bottomRight = collapseBorders(bottomRightCell.topBorder(), table->topBorder());
184 	else if (rightCell.isValid())
185 		*bottomRight = collapseBorders(table->bottomBorder(), rightCell.bottomBorder());
186 	else
187 		*bottomRight = TableBorder();
188 }
189 
collapseBorders(const TableBorder & firstBorder,const TableBorder & secondBorder)190 TableBorder collapseBorders(const TableBorder& firstBorder, const TableBorder& secondBorder)
191 {
192 	TableBorder collapsedBorder;
193 
194 	if (firstBorder.isNull() && secondBorder.isNull())
195 	{
196 		// Both borders are null, so return a null border.
197 		return collapsedBorder;
198 	}
199 	if (firstBorder.isNull())
200 	{
201 		// First border is null, so return second border.
202 		collapsedBorder = secondBorder;
203 	}
204 	else if (secondBorder.isNull())
205 	{
206 		// Second border is null, so return first border.
207 		collapsedBorder = firstBorder;
208 	}
209 	else
210 	{
211 		if (firstBorder.width() > secondBorder.width())
212 		{
213 			// First border is wider than second border, so return first border.
214 			collapsedBorder = firstBorder; // (4)
215 		}
216 		else if (firstBorder.width() < secondBorder.width())
217 		{
218 			// Second border is wider than first border, so return second border.
219 			collapsedBorder = secondBorder; // (5)
220 		}
221 		else
222 		{
223 			if (firstBorder.borderLines().size() > secondBorder.borderLines().size())
224 			{
225 				// First border has more border lines than second border, so return first border.
226 				collapsedBorder = firstBorder;
227 			}
228 			else
229 			{
230 				// Second border has more or equal border lines than first border, so return second border.
231 				collapsedBorder = secondBorder;
232 			}
233 		}
234 	}
235 
236 	return collapsedBorder;
237 }
238 
joinVertical(const TableBorder & border,const TableBorder & topLeft,const TableBorder & top,const TableBorder & topRight,const TableBorder & bottomLeft,const TableBorder & bottom,const TableBorder & bottomRight,QPointF * start,QPointF * end,QPointF * startOffsetFactors,QPointF * endOffsetFactors)239 void joinVertical(const TableBorder& border, const TableBorder& topLeft, const TableBorder& top,
240 				  const TableBorder& topRight, const TableBorder& bottomLeft, const TableBorder& bottom,
241 				  const TableBorder& bottomRight, QPointF* start, QPointF* end, QPointF* startOffsetFactors,
242 				  QPointF* endOffsetFactors)
243 {
244 	Q_ASSERT(start);
245 	Q_ASSERT(end);
246 	Q_ASSERT(startOffsetFactors);
247 	Q_ASSERT(endOffsetFactors);
248 
249 	// Reset offset coefficients.
250 	startOffsetFactors->setX(0.0);
251 	startOffsetFactors->setY(0.0);
252 	endOffsetFactors->setX(0.0);
253 	endOffsetFactors->setY(0.0);
254 
255 	/*
256 	 * The numbered cases in the code below refers to the 45 possible join cases illustrated
257 	 * in the picture at http://wiki.scribus.net/canvas/File:Table_border_join_cases.png
258 	 */
259 
260 	/*
261 	 * Adjust start point(s). Possible cases are 1-20, 26-39.
262 	 */
263 	if (border.joinsWith(topLeft))
264 	{
265 		if (border.joinsWith(topRight))
266 		{
267 			if (!border.joinsWith(top))
268 			{
269 				// Cases: 8, 19.
270 				startOffsetFactors->setY(-0.5);
271 			}
272 		}
273 		else if (!border.joinsWith(top))
274 		{
275 			if (top.joinsWith(topRight))
276 			{
277 				if (border.width() < top.width())
278 				{
279 					// Cases: 15A.
280 					start->setY(start->y() + 0.5 * top.width());
281 				}
282 				else
283 				{
284 					// Cases: 15B.
285 					startOffsetFactors->setY(-0.5);
286 				}
287 			}
288 			else
289 			{
290 				// Cases: 5, 17, 27, 38.
291 				startOffsetFactors->setY(-0.5);
292 			}
293 		}
294 	}
295 	else if (border.joinsWith(topRight))
296 	{
297 		if (!border.joinsWith(top))
298 		{
299 			if (top.joinsWith(topLeft))
300 			{
301 				if (border.width() < top.width())
302 				{
303 					// Cases: 14A.
304 					start->setY(start->y() + 0.5 * top.width());
305 				}
306 				else
307 				{
308 					// Cases: 14B.
309 					startOffsetFactors->setY(-0.5);
310 				}
311 			}
312 			else
313 			{
314 				// Cases: 4, 18, 32, 36.
315 				startOffsetFactors->setY(-0.5);
316 			}
317 		}
318 	}
319 	else if (border.joinsWith(top))
320 	{
321 		if (topLeft.joinsWith(topRight))
322 		{
323 			// Cases: 11.
324 			start->setY(start->y() + 0.5 * topLeft.width());
325 		}
326 	}
327 	else
328 	{
329 		// Cases: 1, 2, 3, 6, 12, 16, 20, 26, 28, 31, 33, 37, 39.
330 		start->setY(start->y() + 0.5 * qMax(topLeft.width(), topRight.width()));
331 	}
332 	// Cases: 7, 9, 10, 13, 29, 30, 34, 35 - No adjustment to start point(s) needed.
333 
334 	/*
335 	 * Adjust end point(s). Possible cases are 1-15, 21-35, 40-43.
336 	 */
337 	if (border.joinsWith(bottomLeft))
338 	{
339 		if (border.joinsWith(bottomRight))
340 		{
341 			if (!border.joinsWith(bottom))
342 			{
343 				// Cases: 6, 24.
344 				endOffsetFactors->setY(0.5);
345 			}
346 		}
347 		else if (!border.joinsWith(bottom))
348 		{
349 			if (bottom.joinsWith(bottomRight))
350 			{
351 				if (bottom.width() < border.width())
352 				{
353 					// Cases: 14A.
354 					endOffsetFactors->setY(0.5);
355 				}
356 				else
357 				{
358 					// Cases: 14B.
359 					end->setY(end->y() - 0.5 * bottom.width());
360 				}
361 			}
362 			else
363 			{
364 				// Cases: 2, 22, 28, 42.
365 				endOffsetFactors->setY(0.5);
366 			}
367 		}
368 	}
369 	else if (border.joinsWith(bottomRight))
370 	{
371 		if (!border.joinsWith(bottom))
372 		{
373 			if (bottom.joinsWith(bottomLeft))
374 			{
375 				if (bottom.width() < border.width())
376 				{
377 					// Cases: 15A.
378 					endOffsetFactors->setY(0.5);
379 				}
380 				else
381 				{
382 					// Cases: 15B.
383 					end->setY(end->y() - 0.5 * bottom.width());
384 				}
385 			}
386 			else
387 			{
388 				// Cases: 3, 23, 33, 40.
389 				endOffsetFactors->setY(0.5);
390 			}
391 		}
392 	}
393 	else if (border.joinsWith(bottom))
394 	{
395 		if (bottomLeft.joinsWith(bottomRight))
396 		{
397 			// Cases: 11.
398 			end->setY(end->y() - 0.5 * bottomLeft.width());
399 		}
400 	}
401 	else
402 	{
403 		// Cases: 1, 4, 5, 8, 12, 21, 25, 26, 27, 31, 32, 41, 43.
404 		end->setY(end->y() - 0.5 * qMax(bottomLeft.width(), bottomRight.width()));
405 	}
406 	// Cases: 7, 9, 10, 13, 29, 30, 34, 35 - No adjustment to end point(s) needed.
407 }
408 
joinHorizontal(const TableBorder & border,const TableBorder & topLeft,const TableBorder & left,const TableBorder & bottomLeft,const TableBorder & topRight,const TableBorder & right,const TableBorder & bottomRight,QPointF * start,QPointF * end,QPointF * startOffsetFactors,QPointF * endOffsetFactors)409 void joinHorizontal(const TableBorder& border, const TableBorder& topLeft, const TableBorder& left,
410 				  const TableBorder& bottomLeft, const TableBorder& topRight, const TableBorder& right,
411 				  const TableBorder& bottomRight, QPointF* start, QPointF* end, QPointF* startOffsetFactors,
412 				  QPointF* endOffsetFactors)
413 {
414 	Q_ASSERT(start);
415 	Q_ASSERT(end);
416 	Q_ASSERT(startOffsetFactors);
417 	Q_ASSERT(endOffsetFactors);
418 
419 	// Reset offset coefficients.
420 	startOffsetFactors->setX(0.0);
421 	startOffsetFactors->setY(0.0);
422 	endOffsetFactors->setX(0.0);
423 	endOffsetFactors->setY(0.0);
424 
425 	/*
426 	 * The numbered cases in the code below refers to the 45 possible join cases illustrated
427 	 * in the picture at http://wiki.scribus.net/canvas/File:Table_border_join_cases.png
428 	 */
429 
430 	/*
431 	 * Adjust start point(s). Possible cases are 1-25, 31-37, 40-41.
432 	 */
433 	if (border.joinsWith(bottomLeft))
434 	{
435 		if (border.joinsWith(topLeft))
436 		{
437 			if (border.joinsWith(left))
438 			{
439 				// Cases: 10.
440 				startOffsetFactors->setX(0.5);
441 			}
442 			else
443 			{
444 				// Cases: 7, 34.
445 				startOffsetFactors->setX(0.5);
446 			}
447 		}
448 		else
449 		{
450 			if (border.joinsWith(left))
451 			{
452 				// Cases: 8, 19.
453 				startOffsetFactors->setX(0.5);
454 			}
455 			else if (left.joinsWith(topLeft))
456 			{
457 				if (border.width() < left.width())
458 				{
459 					// Cases: 14A.
460 					start->setX(start->x() + 0.5 * left.width());
461 				}
462 				else
463 				{
464 					// Cases: 14B.
465 					startOffsetFactors->setX(0.5);
466 				}
467 			}
468 			else
469 			{
470 				// Cases: 4, 18, 32, 36.
471 				startOffsetFactors->setX(0.5);
472 			}
473 		}
474 	}
475 	else if (border.joinsWith(topLeft))
476 	{
477 		if (border.joinsWith(left))
478 		{
479 			// Cases: 6, 24.
480 			startOffsetFactors->setX(0.5);
481 		}
482 		else
483 		{
484 			if (left.joinsWith(bottomLeft))
485 			{
486 				if (left.width() < border.width())
487 				{
488 					// Cases: 15A.
489 					startOffsetFactors->setX(0.5);
490 				}
491 				else
492 				{
493 					// Cases: 15B.
494 					start->setX(start->x() + 0.5 * left.width());
495 				}
496 			}
497 			else
498 			{
499 				// Cases: 3, 23, 33, 40.
500 				startOffsetFactors->setX(0.5);
501 			}
502 		}
503 	}
504 	else if (!border.joinsWith(left) &&
505 			 (topLeft.joinsWith(bottomLeft) || topLeft.joinsWith(left) || bottomLeft.joinsWith(left)))
506 	{
507 		// Cases: 2, 5, 9, 13, 17, 22, 35.
508 		start->setX(start->x() + 0.5 * qMax(topLeft.width(), bottomLeft.width()));
509 	}
510 	else if (left.isNull())
511 	{
512 		// Cases: 31, 37, 41.
513 		start->setX(start->x() - 0.5 * qMax(topLeft.width(), bottomLeft.width()));
514 	}
515 	// Cases: 1, 11, 12, 16, 20, 21, 25 - No adjustment to start point(s) needed.
516 
517 	/*
518 	 * Adjust end point(s). Possible cases are 1-30, 38-39, 42-43.
519 	 */
520 	if (border.joinsWith(bottomRight))
521 	{
522 		if (border.joinsWith(topRight))
523 		{
524 			if (border.joinsWith(right))
525 			{
526 				// Cases: 10.
527 				endOffsetFactors->setX(-0.5);
528 			}
529 			else
530 			{
531 				// Cases: 9, 29.
532 				endOffsetFactors->setX(-0.5);
533 			}
534 		}
535 		else if (border.joinsWith(right))
536 		{
537 			// Cases: 6, 24.
538 			endOffsetFactors->setX(-0.5);
539 		}
540 		else
541 		{
542 			if (right.joinsWith(topRight))
543 			{
544 				if (border.width() < right.width())
545 				{
546 					// Cases: 15A.
547 					end->setX(end->x() - 0.5 * right.width());
548 				}
549 				else
550 				{
551 					// Cases: 15B.
552 					endOffsetFactors->setX(-0.5);
553 				}
554 			}
555 			else
556 			{
557 				// Cases: 5, 17, 27, 38.
558 				endOffsetFactors->setX(-0.5);
559 			}
560 		}
561 	}
562 	else if (border.joinsWith(topRight))
563 	{
564 		if (border.joinsWith(right))
565 		{
566 			// Cases: 6, 24.
567 			endOffsetFactors->setX(-0.5);
568 		}
569 		else
570 		{
571 			if (right.joinsWith(bottomRight))
572 			{
573 				if (right.width() < border.width())
574 				{
575 					// Cases: 14A.
576 					endOffsetFactors->setX(-0.5);
577 				}
578 				else
579 				{
580 					// Cases: 14B.
581 					end->setX(end->x() - 0.5 * right.width());
582 				}
583 			}
584 			else
585 			{
586 				// Cases: 2, 22, 28, 42.
587 				endOffsetFactors->setX(-0.5);
588 			}
589 		}
590 	}
591 	else if (!border.joinsWith(right) &&
592 			 (topRight.joinsWith(bottomRight) || topRight.joinsWith(right) || bottomRight.joinsWith(right)))
593 	{
594 		// Cases: 3, 4, 7, 13, 18, 23, 30.
595 		end->setX(end->x() - 0.5 * qMax(topRight.width(), bottomRight.width()));
596 	}
597 	else if (right.isNull())
598 	{
599 		// Cases: 26, 39, 43.
600 		end->setX(end->x() + 0.5 * qMax(topRight.width(), bottomRight.width()));
601 	}
602 	// Cases: 1, 11, 12, 16, 20, 21, 25 - No adjustment to end point(s) needed.
603 }
604 
605 } // namespace TableUtils
606