-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathtables.lisp
512 lines (475 loc) · 24.1 KB
/
tables.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
;;; cl-typesetting copyright 2002-2021 Marc Battyani see license.txt for the details
;;; You can reach me at [email protected] or [email protected]
;;; The homepage of cl-typesetting is here: http://www.fractalconcept.com/asp/html/cl-typesetting.html
;;; Thanks to Dmitri Ivanov for the splittable tables!
(in-package #:typeset)
(defvar *table* nil)
(defvar *table-row* nil)
(defclass table-cell ()
((content :accessor content :initarg :content)
(box :accessor box)
(width :accessor width :initform 0)
(height :accessor height :initform 0)
(v-align :accessor v-align :initform :top :initarg :v-align)
(background-color :accessor background-color :initform nil :initarg :background-color)
;; col-span equal to 0 (zero) means that the cell spans all rows
;; from the current row to the last row of the table
(col-span :accessor col-span :initform 1 :initarg :col-span)
;; row-span equal to 0 (zero) means that the cell spans all columns
;; from the current column to the last column of the table
(row-span :accessor row-span :initform 1 :initarg :row-span)
;; Quad specifying the border to be drawn between table border and cell padding;
;; Borders of a cell count for its external height (unless it is limited by the height
;; of the containing row) and tighten the width of its content box.
;; Hence, they participate in the calculation of total row or table height,
;; but do not increase column width.
;; If is symbol, has special mening:
;; - T (default) means "inherit" from table (or row?),
;; - NIL, displays no additional border and even supresses the table border.
(border :accessor border :initform t :initarg :border)))
(defclass table-row ()
((height :accessor height :initform nil :initarg :height)
(splittable-p :accessor splittable-p :initform t :initarg :splittable-p)
(background-color :accessor background-color :initform nil :initarg :background-color)
(cells :accessor cells :initform () :initarg :cells)))
(defclass table (box v-mode-mixin)
((cols-widths :accessor col-widths :initform nil :initarg :col-widths)
(h-align :accessor h-align :initform :left :initarg :h-align)
(splittable-p :accessor splittable-p :initform t :initarg :splittable-p)
(border :accessor border :initform 1 :initarg :border)
(border-color :accessor border-color :initform '(0 0 0) :initarg :border-color)
(background-color :accessor background-color :initform nil :initarg :background-color)
(padding :accessor padding :initform 1 :initarg :padding)
(cell-padding :accessor cell-padding :initform 1 :initarg :cell-padding)
(rows :accessor rows :initform ())
(header :accessor header :initform nil) ; rows printed on each page
(footer :accessor footer :initform nil) ; rows printed on each page
(header&footer-height :accessor header&footer-height :initform 0)
))
(defclass split-table (box v-mode-mixin)
((original-table :accessor original-table :initform nil :initarg :original-table)
(rows :accessor rows :initform nil :initarg :rows)))
(defun add-table-row (row &optional (table *table*))
(if (rows table)
(setf (cdr (last (rows table))) (list row))
(setf (rows table) (list row))))
(defun add-table-cell (cell &optional (row *table-row*))
(if (cells row)
(setf (cdr (last (cells row))) (list cell))
(setf (cells row) (list cell))))
(defun first-or-self (arg)
;;; The first element of arg, if it is a list; else arg itself.
;; Code from Paradigms of AI Programming, Copyright (c) 1991 Peter Norvig
(if (consp arg) (first arg) arg))
(defmacro cell-start-row-p (cell row)
`(or (numberp (row-span ,cell)) ; still untouched cell
(eq (first-or-self (row-span ,cell)) ,row)))
(defmacro cell-continue-row-p (cell row)
`(and (consp (row-span ,cell))
(not (eq (first (row-span ,cell)) ,row))))
(defmacro cell-end-row-p (cell row)
`(and (consp (row-span ,cell))
(eq (first (last (row-span ,cell))) ,row)))
(defun span-cell (rows cell col-number)
;;; Replace the cell's numeric row-span by the list of rows spanned
;; and add the cell into the cell list of each of these rows.
;; Args: rows Table row sublist starting from the one where the cell was defined.
;; col-number Starts from zero.
(setf (splittable-p (first rows)) nil)
(do* ((rows-spanned (list (first rows)))
(row-span (row-span cell)) ; zero row-span means all the rows
(i (if (= row-span 0) most-positive-fixnum (1- row-span)) (1- i))
(tail (rest rows) rest)
(rest (rest tail) (rest rest)))
((or (null tail) (= i 0)) ; replace numeric row-span by the list
(setf (row-span cell) (nreverse rows-spanned)))
(let ((row (first tail)))
(push row rows-spanned)
(when (and (> i 1) rest) ; set all but last rows unsplittable
(setf (splittable-p row) nil))
(do* ((head () (cons c head))
(j 0 (+ j (col-span c)))
(tail (cells row) (rest tail))
(c (first tail) (first tail))) ; j is the column number of c
((or (null c) (>= j col-number)) ; insert cell between head and tail
(setf (cells row) (nreconc head (cons cell tail))) )) )))
(defun reduce+nullable (rows &key (key #'height) (initial-value 0) end)
;;; Summarize height of the rows in subsequence (subseq rows 0 length).
;; Returns NIL as soon as some of the height values is null.
(do ((i 0 (1+ i))
(tail rows (rest tail)))
((or (null tail) (and end (>= i end)))
(values initial-value i))
(let* ((elt (first tail))
(value (if key (funcall key elt) elt)))
(if (numberp value)
(incf initial-value value)
(return nil)))))
(defun row-cell-col-number (cell cells)
;;; Value: column number of the cell with cells
(do ((j 0 (+ j col-span)) ; step by col-span
(tail cells (rest tail))
c
col-span)
((null tail)
nil)
(cond ((eq cell (setq c (first tail)))
(return j))
((= (setq col-span (col-span c)) 0)
(return most-positive-fixnum)))))
(defun reorder-row-cells (row)
;;; Sort the row's cells correctly as there are more than one cells that
;; - are defined by the previous row or rows, and
;; - are spanning to this row.
;; Value: new list
(flet ((predicate (cell) (cell-continue-row-p cell row)))
(do* ((cells (cells row))
(continue (sort (mapcar (lambda (cell) ; alist of cells defined above
(cons cell (row-cell-col-number
cell (cells (first (row-span cell))))))
(remove-if-not #'predicate cells))
#'<
:key #'cdr))
(start (remove-if #'predicate cells)) ; list cells defined here
cell
(j 0 (+ j (col-span cell)))
(acc ()))
((cond ((null continue)
(setq acc (nreconc acc start))
t)
((null start)
(setq acc (nreconc acc (mapcar #'car continue)))
t))
acc)
(if (>= j (cdar continue))
(setq cell (caar continue) ; found in above
continue (cdr continue))
(setq cell (pop start))) ; take from current
(push cell acc))))
(defun compute-row-size (table row rows)
(let* ((full-size-offset (+ (border table) (* 2 (cell-padding table))))
(height-expr (height row))
(height (if (numberp height-expr)
height-expr
+huge-number+))
(continued-count 0)) ; nr of spanned cells defined above
(loop with next-widths = (col-widths table)
with col-count = (length next-widths)
for cell in (cells row)
and width = (pop next-widths)
and col-number = 0 then (+ col-number actual-col-span 1)
and cell-height = 0.0
for col-span = (col-span cell)
and row-span = (row-span cell)
and cell-border = (border cell)
for actual-col-span = (1- (if (= col-span 0) ; allowed for the very last column
(- col-count col-number)
col-span))
and cell-borders-width
= (cond ((symbolp cell-border) 0)
((numberp cell-border) (+ cell-border cell-border))
((with-quad (left-border nil right-border) cell-border
(+ left-border right-border))))
and cell-borders-height
= #1=(cond ((symbolp cell-border) 0)
((numberp cell-border) (+ cell-border cell-border))
((with-quad (left-border top-border nil bottom-border) cell-border
(+ top-border bottom-border))))
unless width do (error "Too many cells in this row")
;; Adjust the cell width for cells spanning multiple columns
unless (= actual-col-span 0)
do (incf width (+ (* actual-col-span full-size-offset)
(reduce #'+ next-widths :end actual-col-span)))
(setf next-widths (nthcdr actual-col-span next-widths))
;; Fill in the cell with content as required and when it spans several rows
;; of fixed height, summarize these height values for layout purpose.
if (cell-start-row-p cell row)
do (setf (box cell) (make-filled-vbox (content cell)
(- width cell-borders-width)
(cond ((eql row-span 1)
height)
((numberp row-span)
(multiple-value-bind (height row-span)
(reduce+nullable rows
:end (if (= row-span 0)
most-positive-fixnum
row-span))
(if height
(+ height (* (1- row-span) ; actual row-span
full-size-offset))
+huge-number+)))
((reduce+nullable row-span ; list of rows
:initial-value (* (1- (length row-span))
full-size-offset)))
(+huge-number+))
(v-align cell))
(width cell) width)
else if (cell-continue-row-p cell row)
do (incf continued-count)
end
;; A cell spanning several rows participates only in height calculation
;; of the last row
if (and (numberp row-span) (/= row-span 1)) ; 0 or >1
do (span-cell rows cell col-number)
else unless (numberp height-expr)
if (eql row-span 1)
do (setq cell-height (+ (compute-boxes-natural-size (boxes (box cell)) #'dy)
cell-borders-height))
else if (cell-end-row-p cell row)
do (setq cell-height
(- (+ (compute-boxes-natural-size (boxes (box cell)) #'dy)
cell-borders-height)
(reduce #'+ row-span
:key #'height
:end (1- (length row-span))
:initial-value (* (1- (length row-span))
full-size-offset))))
maximize cell-height into max-height
;finally (setf height (+ (max (or (height row) 0.0) max-height) +epsilon+))
finally (setf height (+ (cond ((numberp height-expr)
(max height-expr max-height))
((consp height-expr)
(apply (first height-expr)
(subst max-height :text-height
(rest height-expr))))
(t max-height))
+epsilon+)))
(setf (height row) height)
(when (> continued-count 1) ; if two or more row-spanned cells are
(setf (cells row) (reorder-row-cells row))) ; continued into this row, sort all cells
(loop for cell in (cells row)
for row-span = (row-span cell)
and cell-border = (border cell)
for cell-borders-height = #1#
if (eql row-span 1)
do (setf (height cell) height
(dy (box cell)) (- height cell-borders-height))
(do-layout (box cell))
else if (cell-end-row-p cell row)
do (let ((height (reduce #'+ row-span :key #'height
:initial-value (* (1- (length row-span))
full-size-offset))))
(setf (height cell) height
(dy (box cell)) (- height cell-borders-height))
(do-layout (box cell))))
height))
(defmethod compute-table-size (table)
(loop for rows on (header table)
do (compute-row-size table (first rows) rows))
(loop for rows on (rows table)
do (compute-row-size table (first rows) rows))
(loop for rows on (footer table)
do (compute-row-size table (first rows) rows))
(let ((nb-h&f (+ (length (header table))(length (footer table))))
(nb-rows (length (rows table)))
(nb-cols (length (col-widths table))))
(setf (header&footer-height table)
(+ (* 2 nb-h&f (cell-padding table))
(* nb-h&f (border table))
(reduce #'+ (header table) :key 'height)
(reduce #'+ (footer table) :key 'height)))
(setf (dx table)(+ (* 2 (padding table))
(* 2 nb-cols (cell-padding table))
(* (1+ nb-cols) (border table))
(reduce #'+ (col-widths table))
+epsilon+)
(dy table)(+ (* 2 (padding table))
(* 2 nb-rows (cell-padding table))
(* (1+ nb-rows) (border table))
(reduce #'+ (rows table) :key 'height)
(header&footer-height table)
+epsilon+))))
(defmethod v-split ((table table) dx dy)
(declare (ignore dx))
"Factor out rows that fit and return a split table + the table."
;; Treat unsplittable rows as a single unit - for this purpose,
;; group the rows list into the following form:
;;
;; ( (group1-height row1 row2 ...)
;; (group2-height row7)
;; (group3-height row8 row9 ...) )
;;
(if (splittable-p table)
(with-slots (header footer border padding cell-padding) table ;(print (rows table))
(loop with fitted-rows = ()
with header&footer-height = (header&footer-height table)
with full-size-offset = (+ (* 2 cell-padding) border)
with current-height = (+ border padding)
with available-height = (- dy header&footer-height)
with row-groups = (loop with height = 0
and rows = ()
for row in (rows table)
do
(incf height (+ (height row) full-size-offset))
(push row rows)
when (splittable-p row)
collect (cons height (nreverse rows))
and do (setf height 0 rows nil))
with rows-remaining = (rows table)
;; Do not count the bottom table padding in available-height.
for (group-height . rows) in row-groups
while (<= (+ current-height group-height) available-height)
do (dolist (r rows)
(push r fitted-rows)
(pop rows-remaining))
(incf current-height group-height)
finally (return
(if fitted-rows
(let ((height (+ current-height header&footer-height)))
;; for split-table height, take bottom padding into account
(incf height (min padding (- available-height height)))
;; reduce rows to output
(setf (rows table) rows-remaining)
;; reduce space required by the rest of the table subtracting
;; neither top border nor bottom table padding
;; nor header/footer
(decf (dy table) (- current-height border padding))
(values (make-instance 'split-table
:rows (nreverse fitted-rows)
:original-table table
:dx (dx table) :dy height)
table
(- dy height)))
(values nil table dy)))))
(values nil table dy)))
(defun multi-line (points widths)
;;; Helper akin to polyline, but skips zero-width lines
;; Args: points - list of dotted(!) pairs.
(do ((prev-x (caar points))
(prev-y (cdar points))
(other-points (rest points) (rest other-points)))
((null (and other-points widths)))
(let ((width (pop widths))
(x (caar other-points))
(y (cdar other-points)))
(if (zerop width)
(setq prev-x x prev-y y)
(progn
(pdf:set-line-width width)
(when prev-x (pdf:move-to prev-x prev-y))
(pdf:line-to x y)
(setq prev-x nil prev-y nil))))))
(defun stroke-table (table x y rows dy)
(let* ((padding (padding table))
(border (border table))
(half-border (/ border 2)) ; for more careful drawing
(*table* table))
(when (background-color table)
(pdf:with-saved-state
(pdf:set-color-fill (background-color table))
(if (or (zerop padding) (zerop border))
(pdf:basic-rect x y (dx table) (- dy))
;; External colorful padding should not breach border
(pdf:basic-rect (+ x padding border) (- y padding border)
(- (dx table) (* 2 (+ padding border)))
(- (* 2 (+ padding border)) dy)))
(pdf:fill-path)))
(loop with cell-padding = (cell-padding table)
with full-size-offset = (+ cell-padding cell-padding border)
for row in (append (header table) rows (footer table))
for row-y = (- y padding border) then (- row-y height full-size-offset)
and height = (height row)
and *table-row* = row
do (loop for cell-x = (+ x padding border) then (+ cell-x width full-size-offset)
for cell in (cells row)
for width = (width cell)
for height = (height cell)
and cell-border = (border cell)
and cell-offset-x = cell-padding
and cell-offset-y = cell-padding
when (cell-start-row-p cell row)
do (pdf:with-saved-state
;; Set translation while counting both table border and padding,
;; but neither cell-border nor cell-padding.
(pdf:translate cell-x row-y)
;; First, fill background
(pdf:with-saved-state
(let ((background-color (or (background-color cell)
(background-color row))))
(when background-color
(pdf:set-color-fill background-color)
(pdf:basic-rect (- half-border) half-border ;0 0
(+ width full-size-offset)
(- (+ height full-size-offset)))
(pdf:fill-path)))
;; Next, draw table border if not suppressed
(unless (or (zerop border) (null cell-border))
(pdf:set-line-width border)
(if (border-color table)
(pdf:set-color-stroke (border-color table))
(pdf:set-gray-stroke 0))
(pdf:basic-rect (- half-border) half-border ;0 0
(+ width full-size-offset)
(- (+ height full-size-offset)))
(pdf:stroke))
;; Last, draw additional cell border
(unless (symbolp cell-border)
(pdf:set-gray-stroke 0)
(with-quad (left-border top-border right-border bottom-border)
cell-border
(if (or (numberp cell-border)
(= left-border top-border right-border bottom-border))
(unless (zerop left-border)
(pdf:set-line-width left-border)
(let ((half-border (/ left-border 2)))
(pdf:basic-rect half-border (- half-border)
(- (+ width full-size-offset) left-border)
(- left-border height full-size-offset))
(incf cell-offset-x left-border)
(incf cell-offset-y left-border)))
(let* ((left (/ left-border 2))
(top (- (/ top-border 2)))
(right (- (+ width full-size-offset)
left (/ right-border 2)))
(bottom (- (/ bottom-border 2) top
height full-size-offset)))
(multi-line `((,left . ,bottom)
(,left . ,top)
(,right . ,top)
(,right . ,bottom)
(,left . ,bottom))
`(,left-border ,top-border
,right-border ,bottom-border))
(incf cell-offset-x left-border)
(incf cell-offset-y top-border))))
(pdf:stroke)))
(stroke cell cell-offset-x (- cell-offset-y)))))))
(defmethod stroke ((table split-table) x y)
(stroke-table (original-table table) x y (rows table) (dy table)))
(defmethod stroke ((table table) x y)
(stroke-table table x y (rows table) (dy table)))
(defmethod stroke ((cell table-cell) x y)
(stroke (box cell) x y))
;;; Convenience macros
(defmacro table ((&key col-widths
(padding 5) (cell-padding 2)
(border 1) (border-color #x00000)
background-color
splittable-p)
&body body)
`(let* ((*table* (make-instance 'table :splittable-p ,splittable-p
:col-widths ,col-widths
:padding ,padding :cell-padding ,cell-padding
:background-color ,background-color
:border ,border :border-color ,border-color)))
(add-box *table*)
,@body
(compute-table-size *table*)
*table*))
(defmacro header-row ((&rest args) &body body)
`(let* ((*table-row* (make-instance 'table-row :splittable-p nil ,@args)))
(setf (header *table*) (nconc (header *table*) (list *table-row*)))
,@body
*table-row*))
(defmacro footer-row ((&rest args) &body body)
`(let* ((*table-row* (make-instance 'table-row :splittable-p nil ,@args)))
(setf (footer *table*) (nconc (footer *table*) (list *table-row*)))
,@body
*table-row*))
(defmacro row ((&rest args) &body body)
`(let ((*table-row* (make-instance 'table-row ,@args)))
(add-table-row *table-row*)
,@body
*table-row*))
(defmacro cell ((&rest args) &body body)
`(add-table-cell (make-instance 'table-cell :content (compile-text () ,@body) ,@args)))