-
Notifications
You must be signed in to change notification settings - Fork 71
/
drawings.jl
1665 lines (1409 loc) · 52.4 KB
/
drawings.jl
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
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
mutable struct Drawing
width::Float64
height::Float64
filename::AbstractString
surface::CairoSurface
cr::CairoContext
surfacetype::Symbol
redvalue::Float64
greenvalue::Float64
bluevalue::Float64
alpha::Float64
buffer::IOBuffer # Keeping both buffer and data because I think the buffer might get GC'ed otherwise
bufferdata::Array{UInt8,1} # Direct access to data
strokescale::Bool
function Drawing(img::Matrix{T}, f::AbstractString = ""; strokescale = false) where {T<:Union{RGB24,ARGB32}}
w, h = size(img)
bufdata = UInt8[]
iobuf = IOBuffer(bufdata, read = true, write = true)
the_surfacetype = :image
the_surface = Cairo.CairoImageSurface(img)
the_cr = Cairo.CairoContext(the_surface)
currentdrawing = new(w, h, f, the_surface, the_cr, the_surfacetype, 0.0, 0.0, 0.0, 1.0, iobuf, bufdata, strokescale)
if !isassigned(_current_drawing(), _current_drawing_index())
push!(_current_drawing(), currentdrawing)
_current_drawing_index(lastindex(_current_drawing()))
else
_current_drawing()[_current_drawing_index()] = currentdrawing
end
return currentdrawing
end
function Drawing(w, h, stype::Symbol, f::AbstractString = ""; strokescale = false)
bufdata = UInt8[]
iobuf = IOBuffer(bufdata, read = true, write = true)
the_surfacetype = stype
if stype == :pdf
the_surface = Cairo.CairoPDFSurface(iobuf, w, h)
elseif stype == :png # default to PNG
the_surface = Cairo.CairoARGBSurface(w, h)
elseif stype == :eps
the_surface = Cairo.CairoEPSSurface(iobuf, w, h)
elseif stype == :svg
the_surface = Cairo.CairoSVGSurface(iobuf, w, h)
elseif stype == :rec
if isnan(w) || isnan(h)
the_surface = Cairo.CairoRecordingSurface()
else
extents = Cairo.CairoRectangle(0.0, 0.0, w, h)
bckg = Cairo.CONTENT_COLOR_ALPHA
the_surface = Cairo.CairoRecordingSurface(bckg, extents)
# Both the CairoSurface and the Drawing stores w and h in mutable structures.
# Cairo.RecordingSurface does not set the w and h properties,
# probably because that could be misinterpreted (width and height
# does not tell everything).
# However, the image_as_matrix() function uses Cairo's values instead of Luxor's.
# Setting these values here is the less clean, less impact solution. NOTE: Switch back
# if revising image_as_matrix to use Drawing: width, height.
the_surface.width = w
the_surface.height = h
end
elseif stype == :image
the_surface = Cairo.CairoImageSurface(w, h, Cairo.FORMAT_ARGB32)
else
error("Unknown Luxor surface type \"$stype\"")
end
the_cr = Cairo.CairoContext(the_surface)
# @info("drawing '$f' ($w w x $h h) created in $(pwd())")
currentdrawing = new(w, h, f, the_surface, the_cr, the_surfacetype, 0.0, 0.0, 0.0, 1.0, iobuf, bufdata, strokescale)
if !isassigned(_current_drawing(), _current_drawing_index())
push!(_current_drawing(), currentdrawing)
_current_drawing_index(lastindex(_current_drawing()))
else
_current_drawing()[_current_drawing_index()] = currentdrawing
end
return currentdrawing
end
end
# we need a thread safe way to store a global stack of drawings and the current active index into this stack
# access to the stack is only possible using the global functions:
# - predefine all needed Dict entries in a thread safe way
# - each thread has it's own stack, separated by threadid
# this is not enough for Threads.@spawn (TODO, but no solution yet)
let _CURRENTDRAWINGS = Ref{Dict{Int,Union{Array{Drawing,1},Nothing}}}(Dict(0 => nothing)),
_CURRENTDRAWINGINDICES = Ref{Dict{Int,Int}}(Dict(0 => 0))
global _current_drawing
function _current_drawing()
id = Threads.threadid()
if !haskey(_CURRENTDRAWINGS[], id)
# predefine all needed Dict entries
lc = ReentrantLock()
lock(lc)
for preID in 1:Threads.nthreads()
_CURRENTDRAWINGS[][preID] = Array{Drawing,1}()
end
unlock(lc)
end
if isnothing(_CURRENTDRAWINGS[][id])
# all Dict entries are predefined, so we should never reach this error
error("(1)thread id should be preallocated")
end
# thread specific stack
return _CURRENTDRAWINGS[][id]
end
global _current_drawing_index
function _current_drawing_index()
id = Threads.threadid()
if !haskey(_CURRENTDRAWINGINDICES[], id)
# predefine all needed Dict entries
lc = ReentrantLock()
lock(lc)
for preID in 1:Threads.nthreads()
_CURRENTDRAWINGINDICES[][preID] = 0
end
unlock(lc)
end
if isnothing(_CURRENTDRAWINGINDICES[][id])
# all Dict entries are predefined, so we should never reach this error
error("(2)thread id should be preallocated")
end
# thread specific current index
return _CURRENTDRAWINGINDICES[][id]
end
function _current_drawing_index(i::Int)
id = Threads.threadid()
if !haskey(_CURRENTDRAWINGINDICES[], id)
# predefine all needed Dict entries
lc = ReentrantLock()
lock(lc)
for preID in 1:Threads.nthreads()
_CURRENTDRAWINGINDICES[][preID] = 0
end
unlock(lc)
end
if isnothing(_CURRENTDRAWINGINDICES[][id])
# all Dict entries are predefined, so we should never reach this error
error("(3)thread id should be preallocated")
end
# set and return the thread specific current index
_CURRENTDRAWINGINDICES[][id] = i
end
global _reset_all_drawings
function _reset_all_drawings()
empty!(_CURRENTDRAWINGS[])
empty!(_CURRENTDRAWINGINDICES[])
return true
end
end
# utility functions that access the internal current Cairo drawing object, which is
# stored as item at index _current_drawing_index() in a constant global array
function _get_current_drawing_save()
# check if drawing is not corrupted (has been observed using PrecompileTools)
# checking fields:
# surface::CairoSurface
# cr::CairoSurface
# not checked but perhaps needed:
# buffer::IOBuffer
# bufferdata::Array{UInt8, 1}
if _current_drawing_index() <= 0 ||
(
_current_drawing_index() > 0 &&
getfield(getfield(_current_drawing()[_current_drawing_index()], :cr), :ptr) == C_NULL &&
getfield(getfield(_current_drawing()[_current_drawing_index()], :surface), :ptr) == C_NULL &&
1 == 1
)
error("There isn't a current drawing. Create one with `Drawing()` or one of the `@draw`/`@png` etc macros.")
end
return _current_drawing()[_current_drawing_index()]
end
function _get_current_cr()
getfield(_get_current_drawing_save(), :cr)
end
_get_current_redvalue() = getfield(_get_current_drawing_save(), :redvalue)
_get_current_greenvalue() = getfield(_get_current_drawing_save(), :greenvalue)
_get_current_bluevalue() = getfield(_get_current_drawing_save(), :bluevalue)
_get_current_alpha() = getfield(_get_current_drawing_save(), :alpha)
function _get_current_color()
d = _get_current_drawing_save()
return (
getfield(d, :redvalue),
getfield(d, :greenvalue),
getfield(d, :bluevalue),
getfield(d, :alpha),
)
end
function _get_current_cr_color()
d = _get_current_drawing_save()
return (
getfield(d, :cr),
getfield(d, :redvalue),
getfield(d, :greenvalue),
getfield(d, :bluevalue),
getfield(d, :alpha),
)
end
_set_current_redvalue(r) = setfield!(_get_current_drawing_save(), :redvalue, convert(Float64, r))
_set_current_greenvalue(g) = setfield!(_get_current_drawing_save(), :greenvalue, convert(Float64, g))
_set_current_bluevalue(b) = setfield!(_get_current_drawing_save(), :bluevalue, convert(Float64, b))
_set_current_alpha(a) = setfield!(_get_current_drawing_save(), :alpha, convert(Float64, a))
function _set_current_color(r, g, b, a)
_set_current_color(r, g, b)
d = _get_current_drawing_save()
setfield!(d, :redvalue, convert(Float64, r))
setfield!(d, :greenvalue, convert(Float64, g))
setfield!(d, :bluevalue, convert(Float64, b))
setfield!(d, :alpha, convert(Float64, a))
end
function _set_current_color(r, g, b)
d = _get_current_drawing_save()
setfield!(d, :redvalue, convert(Float64, r))
setfield!(d, :greenvalue, convert(Float64, g))
setfield!(d, :bluevalue, convert(Float64, b))
end
_current_filename() = getfield(_get_current_drawing_save(), :filename)
_current_width() = getfield(_get_current_drawing_save(), :width)
_current_height() = getfield(_get_current_drawing_save(), :height)
_current_surface() = getfield(_get_current_drawing_save(), :surface)
_current_surface_ptr() = getfield(getfield(_get_current_drawing_save(), :surface), :ptr)
_current_surface_type() = getfield(_get_current_drawing_save(), :surfacetype)
_current_buffer() = getfield(_get_current_drawing_save(), :buffer)
_current_bufferdata() = getfield(_get_current_drawing_save(), :bufferdata)
_get_current_strokescale() = getfield(_get_current_drawing_save(), :strokescale)
_set_current_strokescale(s) = setfield!(_get_current_drawing_save(), :strokescale, s)
"""
Luxor._drawing_indices()
Get a UnitRange over all available indices of drawings.
With Luxor you can work on multiple drawings simultaneously. Each drawing is stored
in an internal array. The first drawing is stored at index 1 when you start a
drawing with `Drawing(...)`. To start a second drawing you call `Luxor._set_next_drawing_index()`,
which returns the new index. Calling another `Drawing(...)` stores the second drawing
at this new index. `Luxor._set_next_drawing_index()` will return and set the next available index
which is available for a new drawing. This can be a new index at the end of drawings, or,
if you already finished a drawing with `finish()`, the index of this finished drawing.
To specify on which drawing the next graphics command should be applied you call
`Luxor._set_drawing_index(i)`. All successive Luxor commands work on this drawing.
With `Luxor._get_drawing_index()` you get the current active drawing index.
Multiple drawings is especially helpful for interactive graphics with live windows
like MiniFB.
Example:
using Luxor
Drawing(500, 500, "1.svg")
origin()
setcolor("red")
circle(Point(0, 0), 100, action = :fill)
Luxor._drawing_indices() # returns 1:1
Luxor._get_next_drawing_index() # returns 2 but doesn't change current drawing
Luxor._set_next_drawing_index() # returns 2 and sets current drawing to it
Drawing(500, 500, "2.svg")
origin()
setcolor("green")
circle(Point(0, 0), 100, action = :fill)
Luxor._drawing_indices() # returns 1:2
Luxor._set_drawing_index(1) # returns 1
finish()
preview() # presents the red circle 1.svg
Luxor._drawing_indices() # returns 1:2
Luxor._set_next_drawing_index() # returns 1 because drawing 1 was finished before
Drawing(500, 500, "3.svg")
origin()
setcolor("blue")
circle(Point(0, 0), 100, action = :fill)
finish()
preview() # presents the blue circle 3.svg
Luxor._set_drawing_index(2) # returns 2
finish()
preview() # presents the green circle 2.svg
Luxor._drawing_indices() # returns 1:2, but all are finished
Luxor._set_drawing_index(1) # returns 1
preview() # presents the blue circle 3.svg again
Luxor._set_drawing_index(10) # returns 1 as 10 does not existing
Luxor._get_drawing_index() # returns 1
Luxor._get_next_drawing_index() # returns 1, because 1 was finished
"""
_drawing_indices() = length(_current_drawing()) == 0 ? (1:1) : (1:length(_current_drawing()))
"""
Luxor._get_drawing_index()
Returns the index of the current drawing. If there isn't any drawing yet returns 1.
"""
_get_drawing_index() = _current_drawing_index() == 0 ? 1 : _current_drawing_index()
"""
Luxor._set_drawing_index(i::Int)
Set the active drawing for successive graphic commands to index i if exist. if index i doesn't exist,
the current drawing is unchanged.
Returns the current drawing index.
Example:
next_index=5
if Luxor._set_drawing_index(next_index) == next_index
# do some additional graphics on the existing drawing
...
else
@warn "Drawing "*string(next_index)*" doesn't exist"
endif
"""
function _set_drawing_index(i::Int)
if isassigned(_current_drawing(), i)
_current_drawing_index(i)
end
return _get_drawing_index()
end
"""
Luxor._get_next_drawing_index()
Returns the next available drawing index. This can either be a new index or an existing
index where a finished (`finish()`) drawing was stored before.
"""
function _get_next_drawing_index()
i = 1
if isempty(_current_drawing())
return i
end
i = findfirst(x -> getfield(getfield(x, :surface), :ptr) == C_NULL, _current_drawing())
if isnothing(i)
return _current_drawing_index() + 1
else
return i
end
end
"""
Luxor._set_next_drawing_index()
Set the current drawing to the next available drawing index. This can either be a new index or an existing
index where a finished (`finish()`) drawing was stored before.
Returns the current drawing index.
"""
function _set_next_drawing_index()
if _has_drawing()
_current_drawing_index(_get_next_drawing_index())
else
return _get_next_drawing_index()
end
return _current_drawing_index()
end
"""
Luxor._has_drawing()
Returns true if there is a current drawing available or finished, otherwise false.
"""
function _has_drawing()
return _current_drawing_index() != 0
end
"""
currentdrawing(d::Drawing)
Sets and returns the current Luxor drawing overwriting an existing drawing if exists.
"""
function currentdrawing(d::Drawing)
if !isassigned(_current_drawing(), _current_drawing_index())
push!(_current_drawing(), d)
_current_drawing_index(lastindex(_current_drawing()))
else
_current_drawing()[_current_drawing_index()] = d
end
return d
end
"""
currentdrawing()
Return the current Luxor drawing, if there currently is one.
"""
function currentdrawing()
if !isassigned(_current_drawing(), _current_drawing_index()) ||
isempty(_current_drawing()) ||
_current_surface_ptr() == C_NULL ||
false
# Already finished or not even started
@info "There is no current drawing"
return false
else
return _current_drawing()[_current_drawing_index()]
end
end
# How Luxor output works. You start by creating a drawing
# either aimed at a file (PDF, EPS, PNG, SVG) or aimed at an
# in-memory buffer (:svg, :png, :rec, or :image); you could be
# working in Jupyter or Pluto or Atom, or a terminal, and on
# either Mac, Linux, or Windows. (The @svg/@png/@pdf macros
# are shortcuts to file-based drawings.) When a drawing is
# finished, you go `finish()` (that's the last line of the
# @... macros.). Then, if you want to actually see it, you
# go `preview()`, which returns the current drawing.
# Then the code has to decide where you're working, and what
# type of file it is, then sends it to the right place,
# depending on the OS.
function Base.show(io::IO, ::MIME"text/plain", d::Drawing)
@debug "show MIME:text/plain"
returnvalue = d.filename
# IJulia call the `show` function twice: once for
# the image MIME and a second time for the text/plain MIME.
# We check if this is such a 'second call':
if get(io, :jupyter, false) &&
(d.surfacetype == :svg || d.surfacetype == :png)
return d.filename
end
if (isdefined(Main, :VSCodeServer) && Main.VSCodeServer.PLOT_PANE_ENABLED[]) && (d.surfacetype == :svg || d.surfacetype == :png)
return d.filename
end
# perhaps drawing hasn't started yet, eg in the REPL
if !ispath(d.filename)
location = !isempty(d.filename) ? d.filename : "in memory"
println(" Luxor drawing: (type = :$(d.surfacetype), width = $(d.width), height = $(d.height), location = $(location))")
else
# open the image file
if Sys.isapple()
run(`open $(returnvalue)`)
elseif Sys.iswindows()
cmd = get(ENV, "COMSPEC", "cmd")
run(`$(ENV["COMSPEC"]) /c start $(returnvalue)`)
elseif Sys.isunix()
run(`xdg-open $(returnvalue)`)
end
end
end
"""
tidysvg(fname)
Read the SVG image in `fname` and write it to a file
`fname-tidy.svg` with modified glyph names.
Return the name of the modified file.
SVG images use 'named defs' for text, which cause errors
problem when used in browsers and notebooks.
See [this github issue](https://github.com/jupyter/notebook/issues/333) for
details.
A kludgy workround is to rename the elements.
"""
function tidysvg(fname)
# I pinched this from Simon's RCall.jl
path, ext = splitext(fname)
outfile = ""
if ext == ".svg"
outfile = "$(path * "-tidy" * ext)"
open(fname) do f
# random alpha strings
r = join(Char.(append!(rand(65:90, 6), rand(97:122, 6))))
d = read(f, String)
d = replace(d, "id=\"glyph" => "id=\"glyph" * r)
d = replace(d, "href=\"#glyph" => "href=\"#glyph" * r)
open(outfile, "w") do out
write(out, d)
end
@debug "modified SVG file copied to $(outfile)"
end
end
return outfile
end
"""
tidysvg(fromfile, tofile)
Read the SVG image in `fromfile` and write it to `tofile` with modified glyph names.
"""
function tidysvg(fromfile, tofile)
path, ext = splitext(fromfile)
if ext == ".svg"
open(fromfile) do f
r = join(Char.(append!(rand(65:90, 6), rand(97:122, 6))))
d = read(f, String)
d = replace(d, "id=\"glyph" => "id=\"glyph" * r)
d = replace(d, "href=\"#glyph" => "href=\"#glyph" * r)
open(tofile, "w") do out
write(out, d)
end
@debug "modified SVG file copied to $(tofile)"
end
end
return tofile
end
# in memory:
Base.showable(::MIME"image/svg+xml", d::Luxor.Drawing) = d.surfacetype == :svg
Base.showable(::MIME"image/png", d::Luxor.Drawing) = d.surfacetype == :png
# prefix all glyphids with a random number
function Base.show(f::IO, ::MIME"image/svg+xml", d::Luxor.Drawing)
@debug "show MIME:svg "
r = string(rand(100000:999999))
# regex is faster
smod = replace(String(d.bufferdata), r"glyph" => "glyph-$r")
write(f, smod)
end
function Base.show(f::IO, ::MIME"image/png", d::Luxor.Drawing)
@debug "show MIME:png "
write(f, d.bufferdata)
end
"""
paper_sizes
The `paper_sizes` Dictionary holds a few paper sizes, width is first, so default is Portrait:
```
"A0" => (2384, 3370),
"A1" => (1684, 2384),
"A2" => (1191, 1684),
"A3" => (842, 1191),
"A4" => (595, 842),
"A5" => (420, 595),
"A6" => (298, 420),
"A" => (612, 792),
"Letter" => (612, 792),
"Legal" => (612, 1008),
"Ledger" => (792, 1224),
"B" => (612, 1008),
"C" => (1584, 1224),
"D" => (2448, 1584),
"E" => (3168, 2448))
```
"""
const paper_sizes = Dict{String,Tuple}(
"A0" => (2384, 3370),
"A1" => (1684, 2384),
"A2" => (1191, 1684),
"A3" => (842, 1191),
"A4" => (595, 842),
"A5" => (420, 595),
"A6" => (298, 420),
"A" => (612, 792),
"Letter" => (612, 792),
"Legal" => (612, 1008),
"Ledger" => (792, 1224),
"B" => (612, 1008),
"C" => (1584, 1224),
"D" => (2448, 1584),
"E" => (3168, 2448))
"""
Create a new drawing, and optionally specify file type (PNG, PDF, SVG, EPS),
file-based or in-memory, and dimensions.
Drawing(width=600, height=600, file="luxor-drawing.png")
# Extended help
```
Drawing()
```
creates a drawing, defaulting to PNG format, default filename "luxor-drawing.png",
default size 800 pixels square.
You can specify dimensions, and assume the default output filename:
```
Drawing(400, 300)
```
creates a drawing 400 pixels wide by 300 pixels high, defaulting to PNG format, default
filename "luxor-drawing.png".
```
Drawing(400, 300, "my-drawing.pdf")
```
creates a PDF drawing in the file "my-drawing.pdf", 400 by 300 pixels.
```
Drawing(1200, 800, "my-drawing.svg")
```
creates an SVG drawing in the file "my-drawing.svg", 1200 by 800 pixels.
```
Drawing(width, height, surfacetype | filename)
```
creates a new drawing of the given surface type (e.g. :svg, :png), storing the picture
only in memory if no filename is provided.
```
Drawing(1200, 1200/Base.Mathconstants.golden, "my-drawing.eps")
```
creates an EPS drawing in the file "my-drawing.eps", 1200 wide by 741.8 pixels (= 1200 ÷ ϕ)
high. Only for PNG files must the dimensions be integers.
```
Drawing("A4", "my-drawing.pdf")
```
creates a drawing in ISO A4 size (595 wide by 842 high) in the file "my-drawing.pdf".
Other sizes available are: "A0", "A1", "A2", "A3", "A4", "A5", "A6", "Letter", "Legal",
"A", "B", "C", "D", "E". Append "landscape" to get the landscape version.
```
Drawing("A4landscape")
```
creates the drawing A4 landscape size.
PDF files default to a white background, but PNG defaults to transparent, unless you specify
one using `background()`.
```
Drawing(width, height, :image)
```
creates the drawing in an image buffer in memory. You can obtain the data as a matrix with
`image_as_matrix()`.
```
Drawing(width, height, :rec)
```
creates the drawing in a recording surface in memory. `snapshot(fname, ...)` to any file format and bounding box,
or render as pixels with `image_as_matrix()`.
```
Drawing(width, height, strokescale=true)
```
creates the drawing and enables stroke scaling (strokes will be scaled according to the current transformation).
(Stroke scaling is disabled by default.)
```
Drawing(img, strokescale=true)
```
creates the drawing from an existing image buffer of type `Matrix{Union{RGB24,ARGB32}}`, e.g.:
```
using Luxor, Colors
buffer=zeros(ARGB32, 100, 100)
d=Drawing(buffer)
```
"""
function Drawing(w = 800.0, h = 800.0, f::AbstractString = "luxor-drawing.png"; strokescale = false)
(path, ext) = splitext(f)
currentdrawing = Drawing(w, h, Symbol(ext[2:end]), f, strokescale = strokescale)
return currentdrawing
end
function Drawing(paper_size::AbstractString, f = "luxor-drawing.png"; strokescale = false)
if occursin("landscape", paper_size)
psize = replace(paper_size, "landscape" => "")
h, w = paper_sizes[psize]
else
w, h = paper_sizes[paper_size]
end
Drawing(w, h, f, strokescale = strokescale)
end
@doc raw"""
_adjust_background_rects(buffer::UInt8[]; addmarker = true)
See issue https://github.com/JuliaGraphics/Luxor.jl/issues/150 for discussion details.
Setting backgrounds in a recording surface (:rec) and creating a svg from it result in elements as
```
<rect x="0" y="0" width="16777215" height="16777215" .../>
```
independent of an existing transformation matrix (e.g. set with origin(...) or snapshot with a crop bounding box).
An existing transformation matrix manifests in the svg file as
```
<use xlink:href="#surface199" transform="matrix(3 1 -1 3 30 40)"/>
```
which is applied to every element including the background rects.
This transformation needs to be inversed for the background rects which is added in this function.
If `addmarker` is not set to false, a class property is set as marker:
```
<rect class="luxor_adjusted" x="0" y="0" width="16777215" height="16777215" .../>
```
"""
function _adjust_background_rects(buffer; addmarker = true)
adjusted_buffer = String(buffer)
# check if there is any transform= part, if not we do not need the next heavy regex
m = match(r"transform=\"matrix\((.+?),(.+?),(.+?),(.+?),(.+?),(.+?)\)\"/>"is, adjusted_buffer)
if !isnothing(m) && length(m.captures) == 6
# get SVG viewbox coordinates to replace the generic 16777215 values
# expected example:
# <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="300pt" height="300pt" viewBox="0 0 300 300" version="1.1">
m = match(r"<svg\s+?[^>]*?viewBox=\"(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\".*?>"is, adjusted_buffer)
adjust_vb = false
if !isnothing(m) && length(m.captures) == 4
(vbx, vby, vbw, vbh) = string.([parse(Float64, m[i]) for i in 1:4])
adjust_vb = true
end
# do adjustment for all <use ...> elements (after <defs>) which have a transform attribute as matrix
# expected example:
# </defs>...<use xlink:href="#surface5" transform="matrix(1,0,0,1,150,150)"/>...</svg>
# xlink:href is deprecated and can be replaced by just href
# a group block with id "surface5" must exist: <g id="surface5" clip-path="url(#clip1)">
# in this group block adjust all background rects with the inverse transform matrix like:
# from:
# <rect x="0" y="0" width="16777215" height="16777215" style="..."/>
# to:
# <rect class="luxor_adjusted" x="0" y="0" width="300" height="300" style="..." transform="matrix(1,0,0,1,-150,-150)"/>
# adding class as verification that tweak was applied.
m = findall(r"<defs\s*?>"is, adjusted_buffer)
# check if there is exactly 1 <defs> element
if !isnothing(m) && length(m) == 1
# get SVG part after </defs> to search for <use ...>
# could be done in a single RegEx but can produce ERROR: PCRE.exec error: match limit exceeded
m = match(r"</defs\s*?>(.*)$"is, adjusted_buffer)
if !isnothing(m) && length(m.captures) == 1
adjusted_buffer_part = m[1]
for m in eachmatch(r"<use[^>]*?(xlink:)*?href=\"#(.*?)\"[^>]*?transform=\"matrix\((.+?),(.+?),(.+?),(.+?),(.+?),(.+?)\)\"/>"is, adjusted_buffer_part)
if !isnothing(m) && length(m.captures) == 8
# id of group block
id = m[2]
# transform matrix applied to all elements in group block
transform = vcat(reshape([parse(Float64, m[i]) for i in 3:8], 2, 3), [0.0 0.0 1.0])
# inverse transform matrix must be applied to background rect to neutralize transform matrix
it = inv(transform)
# get the group block with id into mid::String
(head, mid, tail, split_ok) = _split_string_into_head_mid_tail(adjusted_buffer, id)
if split_ok
# add inverse transform matrix to every background rect
# background rects look like:
# <rect x="0" y="0" width="16777215" height="16777215" style="fill:rgb(0%,69.803922%,93.333333%);fill-opacity:1;stroke:none;"/>
# add class="luxor_adjusted" too, for future reference that element has been tweaked
invtransformstring = "transform=\"matrix(" * join(string.(it[1:2, 1:3][:]), ",") * ")\""
marker = ""
if addmarker
marker = "class=\"luxor_adjusted\" "
end
mid = replace(mid, r"(<rect) (x=\"0\" y=\"0\" width=\"16777215\" height=\"16777215\".*?)/>"is => SubstitutionString("\\1 $(marker)\\2 $(invtransformstring)/>"))
if adjust_vb
# some SVG tools don't like this huge rects (e.g. inkscape)
# => replace 0,0,16777215,16777215 with viewBox coordinates
mid = replace(mid, r"(?<a><rect\s+?.*?\s+?x=\")0(?<b>\" y=\")0(?<c>\" width=\")16777215(?<d>\" height=\")16777215(?<e>\".*?/>)"is => SubstitutionString("\\g<a>$(vbx)\\g<b>$(vby)\\g<c>$(vbw)\\g<d>$(vbh)\\g<e>"))
end
adjusted_buffer = head * mid * tail
end
end
end
end
end
end
adjusted_buffer = UInt8.(collect(adjusted_buffer))
return adjusted_buffer
end
@doc raw"""
_split_string_into_head_mid_tail(s::String,id::String)
splits s into head, mid and tail string.
Example:
```
s="head...<g id="\$id">...</g>...tail"
```
results in
```
head="head..."
mid="<g id="\$id">...</g>"
tail="...tail"
```
"""
function _split_string_into_head_mid_tail(s, id)
head = ""
mid = s
tail = ""
split_ok = false
# find all group start elements <g ...>
startgroups = findall(r"<g(?:>|\s+?.*?>)"is, s)
# find all group end elements </g...>
endgroups = findall(r"</g\s*>"is, s)
# there must be as many start elements as end elements
if length(startgroups) == length(endgroups)
#if length(startgroups) != length(endgroups)
# @warn "number of group starting tags <g ...> do not match number of closing tags </g> in SVG"
#else
# find the group start element with the proper id
first_group = findfirst(Regex("<g\\s+?[^>]*?id=\"$(id)\".*?>", "is"), s)
group_start_index = findfirst(e -> e == first_group, startgroups)
if !isnothing(group_start_index) && !isempty(group_start_index)
mid_start = first_group[1]
group_end_index = 0
# starting with group start element with the proper id traverse the end group elements
# and the start group elements until the number traversed are equal the first time
while group_start_index - group_end_index !== 0 && group_start_index < length(startgroups) && group_end_index <= length(endgroups)
group_end_index += 1
while group_end_index <= length(endgroups) && endgroups[group_end_index][1] < startgroups[group_start_index][1]
group_end_index += 1
end
while group_start_index < length(startgroups) && startgroups[group_start_index + 1][1] < endgroups[group_end_index][1]
group_start_index += 1
end
end
if group_start_index - group_end_index == 0
mid_end = endgroups[group_end_index][end]
# start and end character of mid is found, construct the substrings
if prevind(s, mid_start) > 0
head = s[1:prevind(s, mid_start)]
end
if nextind(s, mid_end) > 0
tail = s[nextind(s, mid_end):end]
end
mid = s[mid_start:mid_end]
split_ok = true
end
end
end
return (head, mid, tail, split_ok)
end
@doc raw"""
finish(;svgpostprocess = false, addmarker = true)
Finish the drawing, close any related files. You may be able to view the drawing
in another application with `preview()`.
For more information about `svgpostprocess` and `addmarker` see help for
`Luxor._adjust_background_rects`
"""
function finish(; svgpostprocess = false, addmarker = true)
if _current_surface_ptr() == C_NULL
# Already finished
return false
end
if _current_surface_type() == :png
Cairo.write_to_png(_current_surface(), _current_buffer())
end
if _current_surface_type() == :image &&
(
typeof(_current_surface()) == Cairo.CairoSurfaceImage{ARGB32} ||
typeof(_current_surface()) == Cairo.CairoSurfaceImage{RGB24}
) &&
endswith(_current_filename(), r"\.png"i)
Cairo.write_to_png(_current_surface(), _current_buffer())
end
Cairo.finish(_current_surface())
Cairo.destroy(_current_surface())
if _current_filename() != ""
if _current_surface_type() != :svg || !svgpostprocess
write(_current_filename(), _current_bufferdata())
else
# next function call adresses the issue in
# https://github.com/JuliaGraphics/Luxor.jl/issues/150
# short: setting a background in svg results in
# <rect x="0" y="0" width="16777215" height="16777215" .../>
# independent of an existing transform matrix (e.g. set with origin(...)
# or snapshot with a negative crop bounding box).
# An existing transform matrix manifests in the svg file as
# <use xlink:href="#surface199" transform="matrix(3 1 -1 3 30 40)"/>
# which is applied to every element including the background rects.
# This transformation needs to be inversed for the background rects
# which is added in this function.
buffer = _adjust_background_rects(copy(_current_bufferdata()); addmarker = addmarker)
# hopefully safe as we are at the end of finish:
_current_drawing()[_current_drawing_index()].bufferdata = buffer
write(_current_filename(), buffer)
end
end
return true
end
@doc raw"""
snapshot(;
fname = :png,
cb = missing,
scalefactor = 1.0,
addmarker = true)
snapshot(fname, cb, scalefactor)
-> finished snapshot drawing, for display
Take a snapshot and save to 'fname' name and suffix. This requires
that the current drawing is a recording surface. You can continue drawing
on the same recording surface.
### Arguments
`fname` the file name or symbol, see [`Drawing`](@ref)
`cb` crop box::BoundingBox - what's inside is copied to snapshot
`scalefactor` snapshot width/crop box width. Same for height.
`addmarker` for more information about `addmarker` see help for `Luxor._adjust_background_rects`
?Luxor._adjust_background_rects
### Examples
```julia
snapshot()
snapshot(fname = "temp.png")
snaphot(fname = :svg)
cb = BoundingBox(Point(0, 0), Point(102.4, 96))
snapshot(cb = cb)
pngdrawing = snapshot(fname = "temp.png", cb = cb, scalefactor = 10)
```
The last example would return and also write a png drawing with 1024 x 960 pixels to storage.
"""
function snapshot(;
fname = :png,
cb = missing,
scalefactor = 1.0,
addmarker = true)
rd = currentdrawing()
isbits(rd) && return false # currentdrawing provided 'info'
if ismissing(cb)
if isnan(rd.width) || isnan(rd.height)
@info "The current recording surface has no bounds. Define a crop box for snapshot."
return false
end
# When no cropping box is given, we take the intention
# to be a snapshot of the entire rectangular surface,
# regardless of recording surface current scaling and rotation.
gsave()
origin()
sn = snapshot(fname, BoundingBox(), scalefactor; addmarker = addmarker)
grestore()
else
if typeof(cb) !== BoundingBox
throw(error("Luxor.snapshot(): $(cb) is not a BoundingBox"))
end
sn = snapshot(fname, cb, scalefactor; addmarker = addmarker)
end
sn
end
function snapshot(fname, cb, scalefactor; addmarker = true)
# Prefix r: recording
# Prefix n: new snapshot
# Device coordinates, device space: (x_d, y_d), origin at top left for Luxor implemented types
# ctm: current transformation matrix - since it's symmetric, Cairo simplifies to a vector.
# User coordinates, user space: (x_u,y_u ) = ctm⁻¹ * (x_d, y_d)
rd = currentdrawing()
isbits(rd) && return false # currentdrawing provided 'info'
rs = _current_surface()
if typeof(rd) !== Drawing
throw(error("Luxor.snapshot(): there is not current drawing"))
end
if _current_surface_type() !== :rec
throw(error("Luxor.snapshot(): drawing type should be :rec"))
end
# The check for an 'alive' drawing should be performed by currentdrawing()
# Working on a dead drawing causes ugly crashes.
# Empty the working buffer to the recording surface:
Cairo.flush(rs)
# Recording surface device origin is assumed to be the
# upper left corner of extents (which is true given how Luxor currently makes these,
# but Cairo now has more options)
# Recording surface current transformation matrix (ctm)