Repository: GiovineItalia/Compose.jl Branch: master Commit: 2a442dd2fcc8 Files: 57 Total size: 429.5 KB Directory structure: gitextract_ye7p7qi7/ ├── .github/ │ └── workflows/ │ ├── CI.yml │ ├── CompatHelper.yml │ └── TagBot.yml ├── .gitignore ├── LICENSE.md ├── Project.toml ├── README.md ├── TODO.md ├── deps/ │ ├── glyphsize.c │ ├── glyphsize.json │ ├── mkfile │ └── snap.svg-min.js ├── docs/ │ ├── Project.toml │ ├── make.jl │ └── src/ │ ├── gallery/ │ │ ├── forms.md │ │ ├── properties.md │ │ └── transforms.md │ ├── index.md │ ├── library.md │ └── tutorial.md ├── src/ │ ├── Compose.jl │ ├── abandoned.jl │ ├── batch.jl │ ├── cairo_backends.jl │ ├── container.jl │ ├── fontfallback.jl │ ├── form.jl │ ├── immerse_backend.jl │ ├── list.jl │ ├── measure.jl │ ├── misc.jl │ ├── pango.jl │ ├── pgf_backend.jl │ ├── property.jl │ ├── stack.jl │ ├── svg.jl │ ├── table-jump.jl │ └── table.jl └── test/ ├── .gitignore ├── Project.toml ├── examples/ │ ├── arc_sector.jl │ ├── arrow.jl │ ├── bezigon.jl │ ├── dashedlines.jl │ ├── forms_and_nans.jl │ ├── golden_rect.jl │ ├── linecaps.jl │ ├── linejoins.jl │ ├── polygon_forms.jl │ ├── primitives.jl │ ├── text.jl │ ├── transformations.jl │ └── unicode.jl ├── immerse.jl ├── misc.jl ├── runtests.jl └── svg.jl ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/CI.yml ================================================ name: CI on: pull_request: branches: - master push: branches: - master tags: '*' jobs: test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: version: - '1.10' - '1' os: - ubuntu-latest - macOS-latest - windows-latest arch: - x64 steps: - uses: actions/checkout@v6 - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v2 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v5 with: files: lcov.info token: ${{ secrets.CODECOV_TOKEN }} docs: name: Documentation runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v2 with: version: '1' - run: | julia --project=docs -e ' using Pkg Pkg.develop(PackageSpec(path=pwd())) Pkg.instantiate()' - run: julia --project=docs docs/make.jl env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} ================================================ FILE: .github/workflows/CompatHelper.yml ================================================ name: CompatHelper on: schedule: - cron: '0 0 * * 0' issues: types: [opened, reopened] jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: julia-version: [1.2.0] julia-arch: [x86] os: [ubuntu-latest] steps: - uses: julia-actions/setup-julia@latest with: version: ${{ matrix.julia-version }} - name: Pkg.add("CompatHelper") run: julia -e 'using Pkg; Pkg.add("CompatHelper")' - name: CompatHelper.main() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: julia -e 'using CompatHelper; CompatHelper.main()' ================================================ FILE: .github/workflows/TagBot.yml ================================================ name: TagBot on: issue_comment: types: - created workflow_dispatch: jobs: TagBot: if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' runs-on: ubuntu-latest steps: - uses: JuliaRegistries/TagBot@v1 with: token: ${{ secrets.TAGBOT_PAT }} ssh: ${{ secrets.DOCUMENTER_KEY }} ================================================ FILE: .gitignore ================================================ *~ *swp # System-specific files and directories generated by the BinaryProvider and BinDeps packages # They contain absolute paths specific to the host computer, and so should not be committed deps/deps.jl deps/build.log deps/downloads/ deps/usr/ deps/src/ # Build artifacts for creating documentation generated by the Documenter package docs/build/ docs/site/ test/output # File generated by Pkg, the package manager, based on a corresponding Project.toml # It records a fixed state of all packages used by the project. As such, it should not be # committed for packages, but should be committed for applications that require a static # environment. Manifest.toml # System files generated by MacOS. .DS_Store ================================================ FILE: LICENSE.md ================================================ Compose is licensed under the MIT License: > Copyright (c) 2012--2015: Daniel C. Jones and other contributors > > Permission is hereby granted, free of charge, to any person obtaining > a copy of this software and associated documentation files (the > "Software"), to deal in the Software without restriction, including > without limitation the rights to use, copy, modify, merge, publish, > distribute, sublicense, and/or sell copies of the Software, and to > permit persons to whom the Software is furnished to do so, subject to > the following conditions: > > The above copyright notice and this permission notice shall be > included in all copies or substantial portions of the Software. > > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND > NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE > LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION > OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION > WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Project.toml ================================================ name = "Compose" uuid = "a81c6b42-2e10-5240-aca2-a61377ecd94b" version = "0.9.6" [deps] Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" IterTools = "c8e1da08-722c-5040-9ed9-7db0dc04731e" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Measures = "442fdcdd-2543-5da2-b0f3-8c86c306513e" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Requires = "ae029012-a4dd-5104-9daa-d747884805df" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [compat] Colors = "0.9, 0.10, 0.11, 0.12, 0.13" DataStructures = "0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19" IterTools = "1" JSON = "0.18, 0.19, 0.20, 0.21" Measures = "0.3" Requires = "0.5, 1.0" julia = "1" ================================================ FILE: README.md ================================================ # Compose! [![][docs-latest-img]][docs-latest-url] [![][travis-img]][travis-url] [![][codecov-img]][codecov-url] Compose is a vector graphics library for Julia. It forms the basis for the statistical graphics system [Gadfly](https://github.com/GiovineItalia/Gadfly.jl). ## Synopsis Unlike most vector graphics libraries, Compose is thoroughly declarative. Rather than issue a sequence of drawing commands, graphics are formed by sticking various things together and then letting the library figure out how to draw it. The "things" in this case fall one of three types: Property, Form, and Canvas. "Sticking together" is primary achieved with the `compose` function. The semantics of composition are fairly simple, and once grasped provide a consistent and powerful means of building vector graphics. ## Documentation - [**LATEST**][docs-latest-url] — *in-development version of the documentation.* [docs-latest-img]: https://img.shields.io/badge/docs-latest-blue.svg [docs-latest-url]: https://giovineitalia.github.io/Compose.jl/latest [travis-img]: http://img.shields.io/travis/GiovineItalia/Compose.jl.svg [travis-url]: https://travis-ci.org/GiovineItalia/Compose.jl [codecov-img]: https://codecov.io/gh/GiovineItalia/Compose.jl/branch/master/graph/badge.svg [codecov-url]: https://codecov.io/gh/GiovineItalia/Compose.jl ================================================ FILE: TODO.md ================================================ * Coherent hstack and vstack functions. * Documentation!!! * Functions for arranging canvases in, e.g. grids. * Embedding fonts in SVGs. ================================================ FILE: deps/glyphsize.c ================================================ /* Use freetype to read a font and dump typeface name and glyph sizes for * printable ascii characters into an easily parsible JSON format. */ #include #include #include FT_FREETYPE_H /* Write a font's glyph extents to stdout. */ void dumpfont(FT_Library library, const char* fn) { FT_Error error; FT_Face face; int face_index; int num_faces = 1; for (face_index = 0; face_index < num_faces; ++face_index) { error = FT_New_Face(library, fn, face_index, &face); if (error) { fprintf(stderr, "Error reading font from %s. (%d)\n", fn, (int) error); exit(1); } num_faces = face->num_faces; /* set dpi to micrometer per inch, so sizes are reported in micrometers. */ const FT_UInt dpi = 25400; error = FT_Set_Char_Size(face, 0, 12*64, dpi, dpi); if (error) { fprintf(stderr, "Error setting font size.\n"); exit(1); } if (strcmp(face->style_name, "Regular") == 0) { printf("\"%s\"", face->family_name); } else { printf("\"%s %s\"", face->family_name, face->style_name); } printf(": {\n \"widths\": {\n "); char c; FT_UInt i; FT_Pos max_height = 0; for (c = 0x20; c <= 0x7e; ++c) { i = FT_Get_Char_Index(face, c); error = FT_Load_Glyph(face, i, FT_LOAD_DEFAULT); if (error) { fprintf(stderr, "Error loading glyph for '%c'.", c); exit(1); } if (face->glyph->metrics.height > max_height) { max_height = face->glyph->metrics.height; } printf(" \""); if (c == '"' || c == '\\') putchar('\\'); printf("%c\": ", c); if (c != '"' && c != '\\') putchar(' '); printf("%5.2f", (double) face->glyph->advance.x / 64.0 / 1000.0); if (c != 0x7e) putchar(','); if ((c - 0x20 + 1) % 5 == 0) { printf("\n "); } } printf("},\n \"height\": %0.2f\n}", (double) max_height / 64.0 / 1000.0); FT_Done_Face(face); if (face_index + 1 < num_faces) puts(","); } } int main(int argc, char* argv[]) { if (argc < 2) { fprintf(stderr, "Usage: glyphsize fontfile [fontfile2...] > stuff.json\n"); return 1; } FT_Library library; FT_Error error; error = FT_Init_FreeType(&library); if (error) { fprintf(stderr, "Error initalizing freetype.\n"); return 1; } puts("{"); int i; for (i = 1; i < argc; ++i) { dumpfont(library, argv[i]); if (i != argc - 1) puts(","); } puts("}"); FT_Done_FreeType(library); return 0; } ================================================ FILE: deps/glyphsize.json ================================================ { "Helvetica Neue Bold": { "widths": { " ": 1.18, "!": 1.18, "\"": 1.96, "#": 2.35, "$": 2.35, "%": 4.23, "&": 2.90, "'": 1.18, "(": 1.25, ")": 1.25, "*": 1.72, "+": 2.54, ",": 1.18, "-": 1.72, ".": 1.18, "/": 1.57, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.54, "=": 2.54, ">": 2.54, "?": 2.35, "@": 3.39, "A": 2.90, "B": 2.98, "C": 3.14, "D": 3.14, "E": 2.74, "F": 2.51, "G": 3.21, "H": 3.14, "I": 1.25, "J": 2.35, "K": 3.06, "L": 2.51, "M": 3.84, "N": 3.14, "O": 3.29, "P": 2.82, "Q": 3.29, "R": 3.06, "S": 2.75, "T": 2.59, "U": 3.14, "V": 2.67, "W": 4.00, "X": 2.82, "Y": 2.82, "Z": 2.74, "[": 1.41, "\\": 1.57, "]": 1.41, "^": 2.54, "_": 2.12, "`": 1.10, "a": 2.43, "b": 2.59, "c": 2.43, "d": 2.59, "e": 2.43, "f": 1.41, "g": 2.59, "h": 2.51, "i": 1.09, "j": 1.18, "k": 2.43, "l": 1.09, "m": 3.84, "n": 2.51, "o": 2.59, "p": 2.59, "q": 2.59, "r": 1.65, "s": 2.27, "t": 1.49, "u": 2.51, "v": 2.20, "w": 3.45, "x": 2.27, "y": 2.20, "z": 2.20, "{": 1.41, "|": 0.94, "}": 1.41, "~": 2.54 }, "height": 4.23 }, "Helvetica Neue": { "widths": { " ": 1.18, "!": 1.10, "\"": 1.80, "#": 2.35, "$": 2.35, "%": 4.23, "&": 2.67, "'": 1.18, "(": 1.10, ")": 1.10, "*": 1.49, "+": 2.54, ",": 1.18, "-": 1.65, ".": 1.18, "/": 1.41, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.54, "=": 2.54, ">": 2.54, "?": 2.35, "@": 3.39, "A": 2.74, "B": 2.90, "C": 3.06, "D": 2.98, "E": 2.59, "F": 2.43, "G": 3.21, "H": 3.06, "I": 1.10, "J": 2.20, "K": 2.82, "L": 2.35, "M": 3.69, "N": 3.06, "O": 3.22, "P": 2.74, "Q": 3.22, "R": 2.90, "S": 2.74, "T": 2.43, "U": 3.06, "V": 2.59, "W": 3.92, "X": 2.59, "Y": 2.74, "Z": 2.59, "[": 1.10, "\\": 1.41, "]": 1.10, "^": 2.54, "_": 2.12, "`": 0.94, "a": 2.27, "b": 2.51, "c": 2.27, "d": 2.51, "e": 2.27, "f": 1.25, "g": 2.43, "h": 2.35, "i": 0.94, "j": 0.94, "k": 2.20, "l": 0.94, "m": 3.61, "n": 2.35, "o": 2.43, "p": 2.51, "q": 2.51, "r": 1.41, "s": 2.12, "t": 1.33, "u": 2.35, "v": 2.12, "w": 3.21, "x": 2.19, "y": 2.12, "z": 2.03, "{": 1.41, "|": 0.94, "}": 1.41, "~": 2.54 }, "height": 4.23 }, "Helvetica Neue UltraLight": { "widths": { " ": 1.18, "!": 0.78, "\"": 1.18, "#": 2.35, "$": 2.35, "%": 2.90, "&": 2.43, "'": 0.71, "(": 0.94, ")": 0.94, "*": 1.49, "+": 2.54, ",": 1.18, "-": 1.41, ".": 1.18, "/": 1.41, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.54, "=": 2.54, ">": 2.54, "?": 2.12, "@": 3.39, "A": 2.43, "B": 2.59, "C": 2.90, "D": 2.74, "E": 2.27, "F": 2.04, "G": 3.06, "H": 2.74, "I": 0.54, "J": 1.96, "K": 2.51, "L": 2.04, "M": 3.30, "N": 2.74, "O": 3.05, "P": 2.43, "Q": 3.05, "R": 2.59, "S": 2.59, "T": 2.12, "U": 2.67, "V": 2.28, "W": 3.68, "X": 2.20, "Y": 2.28, "Z": 2.12, "[": 0.94, "\\": 1.41, "]": 0.94, "^": 2.54, "_": 2.12, "`": 0.55, "a": 2.04, "b": 2.27, "c": 2.12, "d": 2.27, "e": 2.12, "f": 0.86, "g": 2.20, "h": 2.12, "i": 0.55, "j": 0.55, "k": 1.88, "l": 0.55, "m": 3.29, "n": 2.12, "o": 2.20, "p": 2.27, "q": 2.27, "r": 1.10, "s": 1.96, "t": 1.02, "u": 2.12, "v": 1.72, "w": 2.90, "x": 1.72, "y": 1.72, "z": 1.72, "{": 1.41, "|": 0.94, "}": 1.41, "~": 2.54 }, "height": 4.23 }, "Helvetica Neue Italic": { "widths": { " ": 1.18, "!": 1.10, "\"": 1.80, "#": 2.35, "$": 2.35, "%": 3.92, "&": 2.67, "'": 1.18, "(": 1.10, ")": 1.10, "*": 1.49, "+": 2.54, ",": 1.18, "-": 1.65, ".": 1.18, "/": 1.41, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.54, "=": 2.54, ">": 2.54, "?": 2.35, "@": 3.39, "A": 2.82, "B": 2.90, "C": 3.06, "D": 2.98, "E": 2.59, "F": 2.43, "G": 3.21, "H": 3.06, "I": 1.10, "J": 2.20, "K": 2.82, "L": 2.35, "M": 3.68, "N": 3.06, "O": 3.21, "P": 2.74, "Q": 3.21, "R": 2.90, "S": 2.74, "T": 2.43, "U": 3.06, "V": 2.59, "W": 3.92, "X": 2.59, "Y": 2.59, "Z": 2.59, "[": 1.10, "\\": 1.41, "]": 1.10, "^": 2.54, "_": 2.12, "`": 0.94, "a": 2.20, "b": 2.51, "c": 2.27, "d": 2.51, "e": 2.27, "f": 1.25, "g": 2.43, "h": 2.35, "i": 0.94, "j": 0.94, "k": 2.04, "l": 0.94, "m": 3.61, "n": 2.35, "o": 2.43, "p": 2.51, "q": 2.51, "r": 1.41, "s": 2.04, "t": 1.33, "u": 2.35, "v": 2.04, "w": 3.21, "x": 2.04, "y": 2.04, "z": 1.88, "{": 1.41, "|": 0.94, "}": 1.41, "~": 2.54 }, "height": 4.23 }, "Helvetica Neue Light": { "widths": { " ": 1.18, "!": 1.02, "\"": 1.57, "#": 2.35, "$": 2.35, "%": 3.76, "&": 2.59, "'": 1.18, "(": 1.02, ")": 1.02, "*": 1.49, "+": 2.54, ",": 1.18, "-": 1.57, ".": 1.18, "/": 1.41, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.54, "=": 2.54, ">": 2.54, "?": 2.27, "@": 3.39, "A": 2.67, "B": 2.82, "C": 2.98, "D": 2.90, "E": 2.51, "F": 2.27, "G": 3.14, "H": 2.98, "I": 0.94, "J": 2.12, "K": 2.74, "L": 2.27, "M": 3.48, "N": 2.98, "O": 3.14, "P": 2.67, "Q": 3.14, "R": 2.82, "S": 2.67, "T": 2.35, "U": 2.90, "V": 2.51, "W": 3.84, "X": 2.43, "Y": 2.59, "Z": 2.43, "[": 1.02, "\\": 1.41, "]": 1.02, "^": 2.54, "_": 2.12, "`": 0.78, "a": 2.20, "b": 2.43, "c": 2.20, "d": 2.43, "e": 2.20, "f": 1.10, "g": 2.35, "h": 2.27, "i": 0.78, "j": 0.78, "k": 2.12, "l": 0.78, "m": 3.53, "n": 2.27, "o": 2.35, "p": 2.43, "q": 2.43, "r": 1.33, "s": 2.04, "t": 1.25, "u": 2.27, "v": 1.96, "w": 3.14, "x": 2.04, "y": 1.96, "z": 1.96, "{": 1.41, "|": 0.94, "}": 1.41, "~": 2.54 }, "height": 4.23 }, "Helvetica Neue UltraLight Italic": { "widths": { " ": 1.18, "!": 0.78, "\"": 1.18, "#": 2.35, "$": 2.35, "%": 2.90, "&": 2.43, "'": 0.71, "(": 0.94, ")": 0.94, "*": 1.49, "+": 2.54, ",": 1.18, "-": 1.41, ".": 1.18, "/": 1.41, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.54, "=": 2.54, ">": 2.54, "?": 2.12, "@": 3.39, "A": 2.43, "B": 2.59, "C": 2.90, "D": 2.82, "E": 2.20, "F": 2.04, "G": 3.06, "H": 2.74, "I": 0.63, "J": 1.96, "K": 2.51, "L": 2.04, "M": 3.37, "N": 2.82, "O": 3.06, "P": 2.43, "Q": 3.06, "R": 2.51, "S": 2.59, "T": 2.12, "U": 2.74, "V": 2.35, "W": 3.76, "X": 2.20, "Y": 2.20, "Z": 2.12, "[": 0.94, "\\": 1.41, "]": 0.94, "^": 2.54, "_": 2.12, "`": 0.55, "a": 2.04, "b": 2.27, "c": 2.12, "d": 2.27, "e": 2.12, "f": 0.86, "g": 2.20, "h": 2.12, "i": 0.55, "j": 0.55, "k": 1.88, "l": 0.55, "m": 3.29, "n": 2.12, "o": 2.20, "p": 2.27, "q": 2.27, "r": 1.10, "s": 1.96, "t": 1.02, "u": 2.12, "v": 1.72, "w": 2.90, "x": 1.72, "y": 1.72, "z": 1.72, "{": 1.41, "|": 0.94, "}": 1.41, "~": 2.54 }, "height": 4.23 }, "Helvetica Neue Condensed Black": { "widths": { " ": 1.10, "!": 1.33, "\"": 2.04, "#": 2.20, "$": 2.20, "%": 3.29, "&": 2.59, "'": 1.10, "(": 1.33, ")": 1.33, "*": 1.72, "+": 2.54, ",": 1.10, "-": 1.56, ".": 1.10, "/": 1.41, "0": 2.20, "1": 2.20, "2": 2.20, "3": 2.20, "4": 2.20, "5": 2.20, "6": 2.20, "7": 2.20, "8": 2.20, "9": 2.20, ":": 1.10, ";": 1.10, "<": 2.54, "=": 2.54, ">": 2.54, "?": 2.12, "@": 3.39, "A": 2.35, "B": 2.43, "C": 2.27, "D": 2.43, "E": 2.12, "F": 2.04, "G": 2.35, "H": 2.43, "I": 1.10, "J": 2.04, "K": 2.35, "L": 1.96, "M": 3.21, "N": 2.51, "O": 2.35, "P": 2.27, "Q": 2.35, "R": 2.43, "S": 2.27, "T": 1.96, "U": 2.35, "V": 2.20, "W": 3.29, "X": 2.27, "Y": 2.20, "Z": 2.04, "[": 1.33, "\\": 1.41, "]": 1.33, "^": 2.54, "_": 2.12, "`": 1.10, "a": 2.12, "b": 2.20, "c": 2.04, "d": 2.20, "e": 2.04, "f": 1.33, "g": 2.20, "h": 2.20, "i": 1.09, "j": 1.09, "k": 2.12, "l": 1.09, "m": 3.29, "n": 2.20, "o": 2.12, "p": 2.20, "q": 2.20, "r": 1.49, "s": 1.96, "t": 1.33, "u": 2.20, "v": 1.96, "w": 3.13, "x": 2.04, "y": 1.96, "z": 1.88, "{": 1.41, "|": 0.94, "}": 1.41, "~": 2.54 }, "height": 3.79 }, "Helvetica Neue Condensed Bold": { "widths": { " ": 1.02, "!": 1.25, "\"": 1.96, "#": 2.03, "$": 2.03, "%": 3.29, "&": 2.51, "'": 1.10, "(": 1.25, ")": 1.25, "*": 1.65, "+": 2.54, ",": 1.02, "-": 1.57, ".": 1.02, "/": 1.41, "0": 2.03, "1": 2.03, "2": 2.03, "3": 2.03, "4": 2.03, "5": 2.03, "6": 2.03, "7": 2.03, "8": 2.03, "9": 2.03, ":": 1.02, ";": 1.02, "<": 2.54, "=": 2.54, ">": 2.54, "?": 2.04, "@": 3.39, "A": 2.35, "B": 2.35, "C": 2.27, "D": 2.43, "E": 2.04, "F": 1.96, "G": 2.35, "H": 2.35, "I": 1.09, "J": 1.96, "K": 2.27, "L": 1.96, "M": 3.13, "N": 2.43, "O": 2.35, "P": 2.20, "Q": 2.35, "R": 2.35, "S": 2.20, "T": 2.03, "U": 2.28, "V": 2.20, "W": 3.22, "X": 2.27, "Y": 2.20, "Z": 2.04, "[": 1.33, "\\": 1.41, "]": 1.33, "^": 2.54, "_": 2.12, "`": 0.94, "a": 2.04, "b": 2.12, "c": 1.96, "d": 2.12, "e": 1.96, "f": 1.25, "g": 2.12, "h": 2.12, "i": 1.02, "j": 1.02, "k": 2.12, "l": 1.02, "m": 3.21, "n": 2.12, "o": 2.03, "p": 2.12, "q": 2.12, "r": 1.41, "s": 1.88, "t": 1.25, "u": 2.12, "v": 1.88, "w": 2.98, "x": 1.96, "y": 1.88, "z": 1.80, "{": 1.33, "|": 0.94, "}": 1.33, "~": 2.54 }, "height": 3.79 }, "Helvetica Neue Bold Italic": { "widths": { " ": 1.18, "!": 1.25, "\"": 2.04, "#": 2.35, "$": 2.35, "%": 4.08, "&": 2.90, "'": 1.18, "(": 1.25, ")": 1.25, "*": 1.72, "+": 2.54, ",": 1.18, "-": 1.72, ".": 1.18, "/": 1.65, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.54, "=": 2.54, ">": 2.54, "?": 2.43, "@": 3.39, "A": 2.90, "B": 3.06, "C": 3.14, "D": 3.14, "E": 2.82, "F": 2.51, "G": 3.21, "H": 3.14, "I": 1.25, "J": 2.35, "K": 3.06, "L": 2.43, "M": 3.84, "N": 3.14, "O": 3.29, "P": 2.82, "Q": 3.29, "R": 3.06, "S": 2.74, "T": 2.59, "U": 3.14, "V": 2.67, "W": 4.00, "X": 2.82, "Y": 2.74, "Z": 2.74, "[": 1.41, "\\": 1.65, "]": 1.41, "^": 2.54, "_": 2.12, "`": 1.10, "a": 2.43, "b": 2.59, "c": 2.35, "d": 2.59, "e": 2.43, "f": 1.49, "g": 2.59, "h": 2.59, "i": 1.10, "j": 1.10, "k": 2.35, "l": 1.10, "m": 3.84, "n": 2.59, "o": 2.51, "p": 2.59, "q": 2.59, "r": 1.65, "s": 2.20, "t": 1.57, "u": 2.59, "v": 2.20, "w": 3.45, "x": 2.20, "y": 2.20, "z": 2.12, "{": 1.41, "|": 0.94, "}": 1.41, "~": 2.54 }, "height": 4.23 }, "Helvetica Neue Light Italic": { "widths": { " ": 1.18, "!": 1.18, "\"": 1.57, "#": 2.35, "$": 2.35, "%": 3.53, "&": 2.59, "'": 1.18, "(": 1.10, ")": 1.10, "*": 1.49, "+": 2.54, ",": 1.18, "-": 1.57, ".": 1.18, "/": 1.41, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.54, "=": 2.54, ">": 2.54, "?": 2.27, "@": 3.39, "A": 2.67, "B": 2.82, "C": 2.98, "D": 2.90, "E": 2.43, "F": 2.27, "G": 3.14, "H": 2.98, "I": 0.94, "J": 2.12, "K": 2.74, "L": 2.27, "M": 3.61, "N": 2.98, "O": 3.14, "P": 2.67, "Q": 3.14, "R": 2.74, "S": 2.67, "T": 2.35, "U": 2.98, "V": 2.51, "W": 3.84, "X": 2.43, "Y": 2.43, "Z": 2.43, "[": 1.02, "\\": 1.41, "]": 1.02, "^": 2.54, "_": 2.12, "`": 0.78, "a": 2.20, "b": 2.43, "c": 2.20, "d": 2.43, "e": 2.20, "f": 1.10, "g": 2.35, "h": 2.27, "i": 0.78, "j": 0.78, "k": 1.96, "l": 0.78, "m": 3.53, "n": 2.27, "o": 2.35, "p": 2.43, "q": 2.43, "r": 1.33, "s": 2.04, "t": 1.25, "u": 2.27, "v": 1.96, "w": 3.14, "x": 1.96, "y": 1.96, "z": 1.80, "{": 1.41, "|": 0.94, "}": 1.41, "~": 2.54 }, "height": 4.23 }, "Helvetica Neue Medium": { "widths": { " ": 1.18, "!": 1.18, "\"": 1.88, "#": 2.35, "$": 2.35, "%": 4.23, "&": 2.74, "'": 1.18, "(": 1.18, ")": 1.18, "*": 1.57, "+": 2.54, ",": 1.18, "-": 1.65, ".": 1.18, "/": 1.49, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.54, "=": 2.54, ">": 2.54, "?": 2.35, "@": 3.39, "A": 2.82, "B": 2.98, "C": 3.06, "D": 3.06, "E": 2.67, "F": 2.51, "G": 3.21, "H": 3.06, "I": 1.18, "J": 2.27, "K": 2.90, "L": 2.43, "M": 3.76, "N": 3.06, "O": 3.22, "P": 2.82, "Q": 3.22, "R": 2.98, "S": 2.74, "T": 2.51, "U": 3.06, "V": 2.59, "W": 4.00, "X": 2.74, "Y": 2.74, "Z": 2.67, "[": 1.25, "\\": 1.49, "]": 1.25, "^": 2.54, "_": 2.12, "`": 1.02, "a": 2.35, "b": 2.59, "c": 2.35, "d": 2.59, "e": 2.35, "f": 1.33, "g": 2.51, "h": 2.43, "i": 1.02, "j": 1.02, "k": 2.27, "l": 1.02, "m": 3.68, "n": 2.43, "o": 2.51, "p": 2.59, "q": 2.59, "r": 1.49, "s": 2.20, "t": 1.41, "u": 2.43, "v": 2.20, "w": 3.29, "x": 2.27, "y": 2.20, "z": 2.12, "{": 1.25, "|": 0.94, "}": 1.25, "~": 2.54 }, "height": 4.23 }, "Helvetica Neue Thin": { "widths": { " ": 1.18, "!": 1.10, "\"": 1.33, "#": 2.35, "$": 2.35, "%": 3.29, "&": 2.51, "'": 0.86, "(": 1.02, ")": 1.02, "*": 1.49, "+": 2.54, ",": 1.18, "-": 1.49, ".": 1.18, "/": 1.41, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.54, "=": 2.54, ">": 2.54, "?": 2.20, "@": 3.39, "A": 2.51, "B": 2.67, "C": 2.98, "D": 2.82, "E": 2.37, "F": 2.18, "G": 3.14, "H": 2.84, "I": 0.72, "J": 2.04, "K": 2.60, "L": 2.12, "M": 3.42, "N": 2.87, "O": 3.14, "P": 2.50, "Q": 3.14, "R": 2.67, "S": 2.67, "T": 2.20, "U": 2.85, "V": 2.35, "W": 3.76, "X": 2.35, "Y": 2.43, "Z": 2.27, "[": 1.02, "\\": 1.41, "]": 1.02, "^": 2.54, "_": 2.12, "`": 0.71, "a": 2.12, "b": 2.35, "c": 2.20, "d": 2.35, "e": 2.20, "f": 1.02, "g": 2.27, "h": 2.20, "i": 0.71, "j": 0.71, "k": 1.96, "l": 0.71, "m": 3.37, "n": 2.20, "o": 2.27, "p": 2.35, "q": 2.35, "r": 1.18, "s": 2.04, "t": 1.10, "u": 2.20, "v": 1.88, "w": 2.98, "x": 1.88, "y": 1.88, "z": 1.80, "{": 1.41, "|": 0.94, "}": 1.41, "~": 2.54 }, "height": 4.23 }, "Helvetica Neue Thin Italic": { "widths": { " ": 1.18, "!": 0.86, "\"": 1.33, "#": 2.35, "$": 2.35, "%": 3.21, "&": 2.51, "'": 0.86, "(": 1.02, ")": 1.02, "*": 1.49, "+": 2.54, ",": 1.18, "-": 1.49, ".": 1.18, "/": 1.41, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.54, "=": 2.54, ">": 2.54, "?": 2.20, "@": 3.39, "A": 2.59, "B": 2.67, "C": 2.98, "D": 2.90, "E": 2.35, "F": 2.20, "G": 3.14, "H": 2.82, "I": 0.78, "J": 2.04, "K": 2.59, "L": 2.12, "M": 3.45, "N": 2.90, "O": 3.14, "P": 2.51, "Q": 3.14, "R": 2.67, "S": 2.67, "T": 2.20, "U": 2.82, "V": 2.43, "W": 3.84, "X": 2.35, "Y": 2.35, "Z": 2.27, "[": 1.02, "\\": 1.41, "]": 1.02, "^": 2.54, "_": 2.12, "`": 0.71, "a": 2.12, "b": 2.35, "c": 2.20, "d": 2.35, "e": 2.20, "f": 1.02, "g": 2.27, "h": 2.20, "i": 0.71, "j": 0.71, "k": 1.96, "l": 0.71, "m": 3.37, "n": 2.20, "o": 2.27, "p": 2.35, "q": 2.35, "r": 1.18, "s": 1.96, "t": 1.10, "u": 2.20, "v": 1.80, "w": 2.98, "x": 1.80, "y": 1.80, "z": 1.80, "{": 1.41, "|": 0.94, "}": 1.41, "~": 2.54 }, "height": 4.23 }, "Helvetica Neue Medium Italic": { "widths": { " ": 1.18, "!": 1.25, "\"": 1.88, "#": 2.35, "$": 2.35, "%": 4.00, "&": 2.82, "'": 1.18, "(": 1.25, ")": 1.25, "*": 1.65, "+": 2.54, ",": 1.18, "-": 1.65, ".": 1.18, "/": 1.49, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.54, "=": 2.54, ">": 2.54, "?": 2.35, "@": 3.39, "A": 2.90, "B": 2.98, "C": 3.06, "D": 3.06, "E": 2.67, "F": 2.51, "G": 3.21, "H": 3.06, "I": 1.18, "J": 2.27, "K": 2.90, "L": 2.43, "M": 3.76, "N": 3.06, "O": 3.21, "P": 2.82, "Q": 3.21, "R": 2.98, "S": 2.74, "T": 2.51, "U": 3.14, "V": 2.59, "W": 3.92, "X": 2.74, "Y": 2.67, "Z": 2.67, "[": 1.25, "\\": 1.49, "]": 1.25, "^": 2.54, "_": 2.12, "`": 1.02, "a": 2.35, "b": 2.59, "c": 2.35, "d": 2.59, "e": 2.35, "f": 1.33, "g": 2.51, "h": 2.43, "i": 1.02, "j": 1.02, "k": 2.20, "l": 1.02, "m": 3.76, "n": 2.43, "o": 2.51, "p": 2.59, "q": 2.59, "r": 1.57, "s": 2.12, "t": 1.41, "u": 2.43, "v": 2.12, "w": 3.29, "x": 2.12, "y": 2.12, "z": 2.04, "{": 1.25, "|": 0.94, "}": 1.25, "~": 2.54 }, "height": 4.23 }, "Helvetica": { "widths": { " ": 1.18, "!": 1.18, "\"": 1.49, "#": 2.35, "$": 2.35, "%": 3.76, "&": 2.82, "'": 0.81, "(": 1.41, ")": 1.41, "*": 1.65, "+": 2.47, ",": 1.18, "-": 1.41, ".": 1.18, "/": 1.18, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.44, "=": 2.47, ">": 2.44, "?": 2.38, "@": 4.30, "A": 2.82, "B": 2.84, "C": 3.03, "D": 3.02, "E": 2.81, "F": 2.59, "G": 3.25, "H": 3.04, "I": 1.18, "J": 2.12, "K": 2.87, "L": 2.34, "M": 3.53, "N": 3.04, "O": 3.29, "P": 2.81, "Q": 3.29, "R": 3.01, "S": 2.76, "T": 2.59, "U": 3.07, "V": 2.82, "W": 4.00, "X": 2.79, "Y": 2.83, "Z": 2.60, "[": 1.18, "\\": 1.23, "]": 1.18, "^": 1.99, "_": 2.35, "`": 1.41, "a": 2.35, "b": 2.36, "c": 2.12, "d": 2.35, "e": 2.34, "f": 1.18, "g": 2.35, "h": 2.35, "i": 0.94, "j": 0.94, "k": 2.12, "l": 0.94, "m": 3.53, "n": 2.35, "o": 2.34, "p": 2.36, "q": 2.35, "r": 1.41, "s": 2.14, "t": 1.18, "u": 2.33, "v": 2.12, "w": 3.06, "x": 2.12, "y": 2.12, "z": 2.12, "{": 1.41, "|": 1.10, "}": 1.41, "~": 2.48 }, "height": 3.95 }, "Helvetica Bold": { "widths": { " ": 1.18, "!": 1.41, "\"": 2.01, "#": 2.35, "$": 2.35, "%": 3.76, "&": 3.08, "'": 1.01, "(": 1.41, ")": 1.41, "*": 1.65, "+": 2.47, ",": 1.18, "-": 1.38, ".": 1.18, "/": 1.18, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.41, ";": 1.41, "<": 2.45, "=": 2.47, ">": 2.45, "?": 2.59, "@": 4.09, "A": 3.06, "B": 3.04, "C": 3.03, "D": 3.06, "E": 2.82, "F": 2.60, "G": 3.29, "H": 3.05, "I": 1.18, "J": 2.31, "K": 3.06, "L": 2.60, "M": 3.53, "N": 3.05, "O": 3.29, "P": 2.79, "Q": 3.29, "R": 3.00, "S": 2.85, "T": 2.59, "U": 3.05, "V": 2.82, "W": 4.00, "X": 2.82, "Y": 2.82, "Z": 2.57, "[": 1.41, "\\": 1.18, "]": 1.41, "^": 2.47, "_": 2.37, "`": 1.41, "a": 2.37, "b": 2.60, "c": 2.36, "d": 2.59, "e": 2.46, "f": 1.41, "g": 2.59, "h": 2.58, "i": 1.18, "j": 1.18, "k": 2.37, "l": 1.18, "m": 3.77, "n": 2.58, "o": 2.59, "p": 2.60, "q": 2.59, "r": 1.65, "s": 2.36, "t": 1.41, "u": 2.58, "v": 2.35, "w": 3.29, "x": 2.35, "y": 2.35, "z": 2.07, "{": 1.65, "|": 1.19, "}": 1.65, "~": 2.49 }, "height": 3.98 }, "Helvetica Oblique": { "widths": { " ": 1.18, "!": 1.18, "\"": 1.50, "#": 2.35, "$": 2.35, "%": 3.76, "&": 2.82, "'": 0.81, "(": 1.41, ")": 1.41, "*": 1.65, "+": 2.47, ",": 1.18, "-": 1.41, ".": 1.18, "/": 1.18, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.47, "=": 2.47, ">": 2.47, "?": 2.35, "@": 4.30, "A": 2.82, "B": 2.82, "C": 3.06, "D": 3.06, "E": 2.82, "F": 2.59, "G": 3.29, "H": 3.06, "I": 1.18, "J": 2.12, "K": 2.82, "L": 2.35, "M": 3.53, "N": 3.06, "O": 3.29, "P": 2.82, "Q": 3.29, "R": 3.06, "S": 2.82, "T": 2.59, "U": 3.06, "V": 2.82, "W": 4.00, "X": 2.82, "Y": 2.82, "Z": 2.59, "[": 1.18, "\\": 1.18, "]": 1.18, "^": 1.99, "_": 2.35, "`": 1.41, "a": 2.35, "b": 2.35, "c": 2.12, "d": 2.35, "e": 2.35, "f": 1.18, "g": 2.35, "h": 2.35, "i": 0.94, "j": 0.94, "k": 2.12, "l": 0.94, "m": 3.53, "n": 2.35, "o": 2.35, "p": 2.35, "q": 2.35, "r": 1.41, "s": 2.12, "t": 1.18, "u": 2.35, "v": 2.12, "w": 3.06, "x": 2.12, "y": 2.12, "z": 2.12, "{": 1.41, "|": 1.10, "}": 1.41, "~": 2.47 }, "height": 3.95 }, "Helvetica Bold Oblique": { "widths": { " ": 1.18, "!": 1.41, "\"": 2.01, "#": 2.35, "$": 2.35, "%": 3.76, "&": 3.06, "'": 1.01, "(": 1.41, ")": 1.41, "*": 1.65, "+": 2.47, ",": 1.18, "-": 1.41, ".": 1.18, "/": 1.18, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.41, ";": 1.41, "<": 2.47, "=": 2.47, ">": 2.47, "?": 2.59, "@": 4.13, "A": 3.06, "B": 3.06, "C": 3.06, "D": 3.06, "E": 2.82, "F": 2.59, "G": 3.29, "H": 3.06, "I": 1.18, "J": 2.35, "K": 3.06, "L": 2.59, "M": 3.53, "N": 3.06, "O": 3.29, "P": 2.82, "Q": 3.29, "R": 3.06, "S": 2.82, "T": 2.59, "U": 3.06, "V": 2.82, "W": 4.00, "X": 2.82, "Y": 2.82, "Z": 2.59, "[": 1.41, "\\": 1.18, "]": 1.41, "^": 2.47, "_": 2.35, "`": 1.41, "a": 2.35, "b": 2.59, "c": 2.35, "d": 2.59, "e": 2.35, "f": 1.41, "g": 2.59, "h": 2.59, "i": 1.18, "j": 1.18, "k": 2.35, "l": 1.18, "m": 3.76, "n": 2.59, "o": 2.59, "p": 2.59, "q": 2.59, "r": 1.65, "s": 2.35, "t": 1.41, "u": 2.59, "v": 2.35, "w": 3.29, "x": 2.35, "y": 2.35, "z": 2.12, "{": 1.65, "|": 1.18, "}": 1.65, "~": 2.47 }, "height": 3.98 }, "Helvetica Light": { "widths": { " ": 1.18, "!": 1.41, "\"": 1.18, "#": 2.35, "$": 2.35, "%": 3.76, "&": 2.82, "'": 0.94, "(": 1.41, ")": 1.41, "*": 1.65, "+": 2.79, ",": 1.18, "-": 1.41, ".": 1.18, "/": 1.18, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.79, "=": 2.79, ">": 2.79, "?": 2.12, "@": 3.39, "A": 2.82, "B": 2.82, "C": 3.06, "D": 3.06, "E": 2.59, "F": 2.35, "G": 3.29, "H": 3.06, "I": 1.18, "J": 2.12, "K": 2.82, "L": 2.35, "M": 3.53, "N": 3.06, "O": 3.29, "P": 2.59, "Q": 3.29, "R": 2.82, "S": 2.59, "T": 2.35, "U": 3.06, "V": 2.59, "W": 3.76, "X": 2.59, "Y": 2.59, "Z": 2.59, "[": 1.41, "\\": 1.18, "]": 1.41, "^": 2.79, "_": 2.12, "`": 1.41, "a": 2.35, "b": 2.59, "c": 2.35, "d": 2.59, "e": 2.35, "f": 1.18, "g": 2.59, "h": 2.35, "i": 0.94, "j": 0.94, "k": 2.12, "l": 0.94, "m": 3.53, "n": 2.35, "o": 2.35, "p": 2.59, "q": 2.59, "r": 1.41, "s": 2.12, "t": 1.18, "u": 2.35, "v": 2.12, "w": 3.06, "x": 2.12, "y": 2.12, "z": 2.12, "{": 1.41, "|": 0.94, "}": 1.41, "~": 2.79 }, "height": 4.23 }, "Helvetica Light Oblique": { "widths": { " ": 1.18, "!": 1.41, "\"": 1.18, "#": 2.35, "$": 2.35, "%": 3.76, "&": 2.82, "'": 0.94, "(": 1.41, ")": 1.41, "*": 1.65, "+": 2.79, ",": 1.18, "-": 1.41, ".": 1.18, "/": 1.18, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.79, "=": 2.79, ">": 2.79, "?": 2.12, "@": 3.39, "A": 2.82, "B": 2.82, "C": 3.06, "D": 3.06, "E": 2.59, "F": 2.35, "G": 3.29, "H": 3.06, "I": 1.18, "J": 2.12, "K": 2.82, "L": 2.35, "M": 3.53, "N": 3.06, "O": 3.29, "P": 2.59, "Q": 3.29, "R": 2.82, "S": 2.59, "T": 2.35, "U": 3.06, "V": 2.59, "W": 3.76, "X": 2.59, "Y": 2.59, "Z": 2.59, "[": 1.41, "\\": 1.18, "]": 1.41, "^": 2.79, "_": 2.12, "`": 1.41, "a": 2.35, "b": 2.59, "c": 2.35, "d": 2.59, "e": 2.35, "f": 1.18, "g": 2.59, "h": 2.35, "i": 0.94, "j": 0.94, "k": 2.12, "l": 0.94, "m": 3.53, "n": 2.35, "o": 2.35, "p": 2.59, "q": 2.59, "r": 1.41, "s": 2.12, "t": 1.18, "u": 2.35, "v": 2.12, "w": 3.06, "x": 2.12, "y": 2.12, "z": 2.12, "{": 1.41, "|": 1.70, "}": 1.41, "~": 2.79 }, "height": 4.63 }, "Arial": { "widths": { " ": 1.18, "!": 1.18, "\"": 1.50, "#": 2.35, "$": 2.35, "%": 3.76, "&": 2.82, "'": 0.81, "(": 1.41, ")": 1.41, "*": 1.65, "+": 2.47, ",": 1.18, "-": 1.41, ".": 1.18, "/": 1.18, "0": 2.35, "1": 2.35, "2": 2.35, "3": 2.35, "4": 2.35, "5": 2.35, "6": 2.35, "7": 2.35, "8": 2.35, "9": 2.35, ":": 1.18, ";": 1.18, "<": 2.47, "=": 2.47, ">": 2.47, "?": 2.35, "@": 4.30, "A": 2.82, "B": 2.82, "C": 3.06, "D": 3.06, "E": 2.82, "F": 2.59, "G": 3.29, "H": 3.06, "I": 1.18, "J": 2.12, "K": 2.82, "L": 2.35, "M": 3.53, "N": 3.06, "O": 3.29, "P": 2.82, "Q": 3.29, "R": 3.06, "S": 2.82, "T": 2.59, "U": 3.06, "V": 2.82, "W": 4.00, "X": 2.82, "Y": 2.82, "Z": 2.59, "[": 1.18, "\\": 1.18, "]": 1.18, "^": 1.99, "_": 2.35, "`": 1.41, "a": 2.35, "b": 2.35, "c": 2.12, "d": 2.35, "e": 2.35, "f": 1.18, "g": 2.35, "h": 2.35, "i": 0.94, "j": 0.94, "k": 2.12, "l": 0.94, "m": 3.53, "n": 2.35, "o": 2.35, "p": 2.35, "q": 2.35, "r": 1.41, "s": 2.12, "t": 1.18, "u": 2.35, "v": 2.12, "w": 3.06, "x": 2.12, "y": 2.12, "z": 2.12, "{": 1.41, "|": 1.10, "}": 1.41, "~": 2.47 }, "height": 3.98 }, "PT Sans": { "widths": { " ": 1.13, "!": 1.29, "\"": 1.42, "#": 2.31, "$": 2.31, "%": 3.27, "&": 3.45, "'": 0.92, "(": 1.19, ")": 1.19, "*": 1.49, "+": 2.14, ",": 0.82, "-": 1.52, ".": 0.91, "/": 1.50, "0": 2.31, "1": 2.31, "2": 2.31, "3": 2.31, "4": 2.31, "5": 2.31, "6": 2.31, "7": 2.31, "8": 2.31, "9": 2.31, ":": 0.93, ";": 1.08, "<": 2.14, "=": 2.14, ">": 2.14, "?": 1.85, "@": 4.50, "A": 2.48, "B": 2.47, "C": 2.42, "D": 2.77, "E": 2.27, "F": 2.19, "G": 2.59, "H": 2.85, "I": 1.23, "J": 1.23, "K": 2.58, "L": 2.19, "M": 3.35, "N": 2.85, "O": 2.90, "P": 2.37, "Q": 2.90, "R": 2.52, "S": 2.25, "T": 2.35, "U": 2.76, "V": 2.40, "W": 3.50, "X": 2.62, "Y": 2.36, "Z": 2.31, "[": 1.29, "\\": 1.61, "]": 1.29, "^": 2.12, "_": 1.73, "`": 1.19, "a": 2.10, "b": 2.29, "c": 1.91, "d": 2.28, "e": 2.15, "f": 1.35, "g": 2.27, "h": 2.31, "i": 1.13, "j": 1.13, "k": 2.03, "l": 1.24, "m": 3.44, "n": 2.31, "o": 2.27, "p": 2.29, "q": 2.27, "r": 1.44, "s": 1.78, "t": 1.44, "u": 2.28, "v": 2.04, "w": 3.11, "x": 2.18, "y": 1.97, "z": 1.89, "{": 1.47, "|": 1.01, "}": 1.47, "~": 2.14 }, "height": 3.94 }, "PT Sans Italic": { "widths": { " ": 1.13, "!": 1.16, "\"": 1.28, "#": 2.20, "$": 2.20, "%": 3.12, "&": 3.27, "'": 0.85, "(": 1.13, ")": 1.13, "*": 1.41, "+": 2.03, ",": 0.79, "-": 1.45, ".": 0.89, "/": 1.42, "0": 2.20, "1": 2.20, "2": 2.20, "3": 2.20, "4": 2.20, "5": 2.20, "6": 2.20, "7": 2.20, "8": 2.20, "9": 2.20, ":": 1.13, ";": 1.16, "<": 2.03, "=": 2.03, ">": 2.03, "?": 1.75, "@": 4.50, "A": 2.36, "B": 2.35, "C": 2.30, "D": 2.63, "E": 2.16, "F": 2.08, "G": 2.47, "H": 2.71, "I": 1.17, "J": 1.18, "K": 2.44, "L": 2.08, "M": 3.19, "N": 2.72, "O": 2.76, "P": 2.25, "Q": 2.76, "R": 2.40, "S": 2.14, "T": 2.23, "U": 2.62, "V": 2.29, "W": 3.34, "X": 2.49, "Y": 2.25, "Z": 2.20, "[": 1.23, "\\": 1.61, "]": 1.24, "^": 2.12, "_": 1.73, "`": 1.01, "a": 2.06, "b": 2.21, "c": 1.78, "d": 2.15, "e": 1.96, "f": 1.18, "g": 2.14, "h": 2.24, "i": 1.10, "j": 1.05, "k": 1.90, "l": 1.14, "m": 3.23, "n": 2.21, "o": 2.11, "p": 2.15, "q": 2.13, "r": 1.38, "s": 1.69, "t": 1.32, "u": 2.16, "v": 1.89, "w": 2.90, "x": 2.05, "y": 1.86, "z": 1.82, "{": 1.38, "|": 1.01, "}": 1.40, "~": 2.03 }, "height": 3.94 }, "PT Sans Narrow Bold": { "widths": { " ": 0.87, "!": 1.06, "\"": 1.49, "#": 2.02, "$": 2.02, "%": 3.05, "&": 2.84, "'": 0.92, "(": 1.18, ")": 1.18, "*": 1.37, "+": 1.88, ",": 0.83, "-": 1.26, ".": 0.81, "/": 1.48, "0": 2.02, "1": 2.02, "2": 2.02, "3": 2.02, "4": 2.02, "5": 2.02, "6": 2.02, "7": 2.02, "8": 2.02, "9": 2.02, ":": 1.06, ";": 1.06, "<": 1.88, "=": 1.88, ">": 1.88, "?": 1.62, "@": 3.73, "A": 2.12, "B": 2.07, "C": 1.93, "D": 2.27, "E": 1.82, "F": 1.77, "G": 2.12, "H": 2.26, "I": 1.01, "J": 1.22, "K": 2.20, "L": 1.79, "M": 2.81, "N": 2.29, "O": 2.34, "P": 2.02, "Q": 2.34, "R": 2.13, "S": 1.86, "T": 2.00, "U": 2.18, "V": 2.13, "W": 3.04, "X": 2.23, "Y": 2.10, "Z": 1.88, "[": 1.17, "\\": 1.53, "]": 1.16, "^": 1.81, "_": 1.58, "`": 1.23, "a": 1.75, "b": 1.88, "c": 1.48, "d": 1.88, "e": 1.81, "f": 1.13, "g": 1.88, "h": 1.90, "i": 0.95, "j": 0.95, "k": 1.74, "l": 1.03, "m": 2.81, "n": 1.90, "o": 1.88, "p": 1.89, "q": 1.88, "r": 1.24, "s": 1.48, "t": 1.23, "u": 1.89, "v": 1.69, "w": 2.52, "x": 1.86, "y": 1.69, "z": 1.61, "{": 1.32, "|": 0.80, "}": 1.32, "~": 1.88 }, "height": 3.94 }, "PT Sans Narrow": { "widths": { " ": 0.90, "!": 1.05, "\"": 1.21, "#": 1.91, "$": 1.91, "%": 2.70, "&": 2.78, "'": 0.77, "(": 0.99, ")": 0.99, "*": 1.23, "+": 1.76, ",": 0.69, "-": 1.23, ".": 0.66, "/": 1.25, "0": 1.91, "1": 1.91, "2": 1.91, "3": 1.91, "4": 1.91, "5": 1.91, "6": 1.91, "7": 1.91, "8": 1.91, "9": 1.91, ":": 0.80, ";": 0.91, "<": 1.76, "=": 1.76, ">": 1.76, "?": 1.51, "@": 3.63, "A": 2.00, "B": 1.99, "C": 1.94, "D": 2.22, "E": 1.82, "F": 1.75, "G": 2.08, "H": 2.27, "I": 0.99, "J": 1.02, "K": 2.09, "L": 1.76, "M": 2.70, "N": 2.28, "O": 2.32, "P": 1.91, "Q": 2.32, "R": 2.03, "S": 1.81, "T": 1.89, "U": 2.21, "V": 1.96, "W": 2.85, "X": 2.12, "Y": 1.93, "Z": 1.86, "[": 1.05, "\\": 1.34, "]": 1.05, "^": 1.74, "_": 1.41, "`": 1.08, "a": 1.69, "b": 1.84, "c": 1.51, "d": 1.83, "e": 1.75, "f": 1.09, "g": 1.83, "h": 1.86, "i": 0.92, "j": 0.91, "k": 1.64, "l": 0.99, "m": 2.76, "n": 1.86, "o": 1.83, "p": 1.84, "q": 1.83, "r": 1.16, "s": 1.43, "t": 1.16, "u": 1.84, "v": 1.64, "w": 2.50, "x": 1.74, "y": 1.59, "z": 1.52, "{": 1.19, "|": 0.80, "}": 1.19, "~": 1.76 }, "height": 3.94 }, "PT Sans Caption Bold": { "widths": { " ": 1.17, "!": 1.33, "\"": 1.85, "#": 2.50, "$": 2.50, "%": 3.84, "&": 3.75, "'": 1.17, "(": 1.51, ")": 1.50, "*": 1.73, "+": 2.41, ",": 1.05, "-": 1.66, ".": 1.02, "/": 1.88, "0": 2.50, "1": 2.50, "2": 2.50, "3": 2.50, "4": 2.50, "5": 2.50, "6": 2.50, "7": 2.50, "8": 2.50, "9": 2.50, ":": 1.24, ";": 1.19, "<": 2.42, "=": 2.42, ">": 2.42, "?": 2.08, "@": 4.86, "A": 2.79, "B": 2.71, "C": 2.57, "D": 2.97, "E": 2.40, "F": 2.33, "G": 2.80, "H": 2.99, "I": 1.64, "J": 1.53, "K": 2.84, "L": 2.36, "M": 3.67, "N": 3.02, "O": 3.08, "P": 2.62, "Q": 3.08, "R": 2.76, "S": 2.43, "T": 2.61, "U": 2.87, "V": 2.77, "W": 3.97, "X": 2.90, "Y": 2.73, "Z": 2.47, "[": 1.48, "\\": 1.95, "]": 1.48, "^": 2.27, "_": 2.04, "`": 1.48, "a": 2.45, "b": 2.62, "c": 2.12, "d": 2.66, "e": 2.51, "f": 1.57, "g": 2.62, "h": 2.70, "i": 1.34, "j": 1.34, "k": 2.39, "l": 1.43, "m": 3.98, "n": 2.70, "o": 2.61, "p": 2.64, "q": 2.62, "r": 1.72, "s": 2.04, "t": 1.71, "u": 2.72, "v": 2.31, "w": 3.49, "x": 2.48, "y": 2.29, "z": 2.23, "{": 1.71, "|": 1.04, "}": 1.71, "~": 2.42 }, "height": 3.99 }, "PT Sans Caption": { "widths": { " ": 1.23, "!": 1.40, "\"": 1.59, "#": 2.51, "$": 2.51, "%": 3.62, "&": 3.77, "'": 1.03, "(": 1.33, ")": 1.32, "*": 1.64, "+": 2.35, ",": 0.93, "-": 1.67, ".": 1.01, "/": 1.68, "0": 2.51, "1": 2.51, "2": 2.51, "3": 2.51, "4": 2.51, "5": 2.51, "6": 2.51, "7": 2.51, "8": 2.51, "9": 2.51, ":": 1.16, ";": 1.17, "<": 2.35, "=": 2.35, ">": 2.35, "?": 2.03, "@": 4.92, "A": 2.73, "B": 2.71, "C": 2.64, "D": 3.03, "E": 2.48, "F": 2.39, "G": 2.83, "H": 3.10, "I": 1.69, "J": 1.38, "K": 2.83, "L": 2.40, "M": 3.67, "N": 3.11, "O": 3.16, "P": 2.60, "Q": 3.16, "R": 2.76, "S": 2.46, "T": 2.58, "U": 3.00, "V": 2.66, "W": 3.87, "X": 2.87, "Y": 2.61, "Z": 2.52, "[": 1.42, "\\": 1.78, "]": 1.42, "^": 2.35, "_": 1.91, "`": 1.27, "a": 2.46, "b": 2.66, "c": 2.24, "d": 2.66, "e": 2.53, "f": 1.54, "g": 2.65, "h": 2.71, "i": 1.34, "j": 1.32, "k": 2.36, "l": 1.42, "m": 4.01, "n": 2.71, "o": 2.67, "p": 2.67, "q": 2.65, "r": 1.61, "s": 2.11, "t": 1.71, "u": 2.73, "v": 2.27, "w": 3.56, "x": 2.51, "y": 2.25, "z": 2.18, "{": 1.62, "|": 1.09, "}": 1.62, "~": 2.35 }, "height": 4.01 }, "PT Sans Bold Italic": { "widths": { " ": 1.07, "!": 1.20, "\"": 1.50, "#": 2.31, "$": 2.31, "%": 3.40, "&": 3.31, "'": 0.92, "(": 1.31, ")": 1.31, "*": 1.51, "+": 2.14, ",": 0.91, "-": 1.45, ".": 1.06, "/": 1.64, "0": 2.31, "1": 2.31, "2": 2.31, "3": 2.31, "4": 2.31, "5": 2.31, "6": 2.31, "7": 2.31, "8": 2.31, "9": 2.31, ":": 1.33, ";": 1.30, "<": 2.14, "=": 2.14, ">": 2.14, "?": 1.82, "@": 4.48, "A": 2.46, "B": 2.40, "C": 2.27, "D": 2.62, "E": 2.13, "F": 2.07, "G": 2.48, "H": 2.64, "I": 1.18, "J": 1.37, "K": 2.51, "L": 2.10, "M": 3.25, "N": 2.67, "O": 2.73, "P": 2.33, "Q": 2.73, "R": 2.47, "S": 2.16, "T": 2.31, "U": 2.55, "V": 2.45, "W": 3.50, "X": 2.57, "Y": 2.41, "Z": 2.19, "[": 1.33, "\\": 1.79, "]": 1.32, "^": 2.14, "_": 1.88, "`": 1.01, "a": 2.13, "b": 2.17, "c": 1.74, "d": 2.15, "e": 1.97, "f": 1.25, "g": 2.14, "h": 2.23, "i": 1.08, "j": 1.03, "k": 1.97, "l": 1.15, "m": 3.25, "n": 2.21, "o": 2.10, "p": 2.14, "q": 2.12, "r": 1.45, "s": 1.68, "t": 1.35, "u": 2.17, "v": 1.90, "w": 2.87, "x": 2.10, "y": 1.91, "z": 1.84, "{": 1.51, "|": 0.98, "}": 1.52, "~": 2.14 }, "height": 3.94 }, "PT Sans Bold": { "widths": { " ": 1.07, "!": 1.25, "\"": 1.70, "#": 2.40, "$": 2.40, "%": 3.56, "&": 3.45, "'": 1.07, "(": 1.38, ")": 1.38, "*": 1.60, "+": 2.23, ",": 0.96, "-": 1.52, ".": 1.04, "/": 1.73, "0": 2.40, "1": 2.40, "2": 2.40, "3": 2.40, "4": 2.40, "5": 2.40, "6": 2.40, "7": 2.40, "8": 2.40, "9": 2.40, ":": 1.14, ";": 1.23, "<": 2.23, "=": 2.23, ">": 2.23, "?": 1.92, "@": 4.48, "A": 2.56, "B": 2.50, "C": 2.37, "D": 2.74, "E": 2.21, "F": 2.15, "G": 2.58, "H": 2.76, "I": 1.21, "J": 1.41, "K": 2.62, "L": 2.18, "M": 3.39, "N": 2.78, "O": 2.84, "P": 2.42, "Q": 2.84, "R": 2.55, "S": 2.24, "T": 2.40, "U": 2.65, "V": 2.55, "W": 3.65, "X": 2.68, "Y": 2.51, "Z": 2.28, "[": 1.37, "\\": 1.79, "]": 1.37, "^": 2.12, "_": 1.88, "`": 1.34, "a": 2.10, "b": 2.27, "c": 1.83, "d": 2.28, "e": 2.15, "f": 1.36, "g": 2.27, "h": 2.31, "i": 1.14, "j": 1.13, "k": 2.08, "l": 1.25, "m": 3.41, "n": 2.31, "o": 2.27, "p": 2.29, "q": 2.27, "r": 1.49, "s": 1.79, "t": 1.48, "u": 2.28, "v": 2.02, "w": 3.05, "x": 2.22, "y": 2.02, "z": 1.94, "{": 1.57, "|": 0.96, "}": 1.57, "~": 2.23 }, "height": 3.94 }, "Source Sans Pro": { "widths": { " ": 0.85, "!": 1.22, "\"": 1.80, "#": 2.10, "$": 2.10, "%": 3.49, "&": 2.58, "'": 1.05, "(": 1.28, ")": 1.28, "*": 1.77, "+": 2.10, ",": 1.05, "-": 1.32, ".": 1.05, "/": 1.48, "0": 2.10, "1": 2.10, "2": 2.10, "3": 2.10, "4": 2.10, "5": 2.10, "6": 2.10, "7": 2.10, "8": 2.10, "9": 2.10, ":": 1.05, ";": 1.05, "<": 2.10, "=": 2.10, ">": 2.10, "?": 1.80, "@": 3.59, "A": 2.28, "B": 2.47, "C": 2.40, "D": 2.58, "E": 2.21, "F": 2.07, "G": 2.59, "H": 2.74, "I": 1.09, "J": 2.01, "K": 2.43, "L": 2.04, "M": 3.06, "N": 2.72, "O": 2.79, "P": 2.42, "Q": 2.79, "R": 2.44, "S": 2.24, "T": 2.25, "U": 2.71, "V": 2.16, "W": 3.31, "X": 2.15, "Y": 1.99, "Z": 2.26, "[": 1.28, "\\": 1.48, "]": 1.28, "^": 2.10, "_": 2.12, "`": 2.29, "a": 2.17, "b": 2.35, "c": 1.93, "d": 2.35, "e": 2.11, "f": 1.24, "g": 2.13, "h": 2.30, "i": 1.04, "j": 1.05, "k": 2.10, "l": 1.08, "m": 3.51, "n": 2.32, "o": 2.29, "p": 2.35, "q": 2.33, "r": 1.47, "s": 1.77, "t": 1.43, "u": 2.32, "v": 1.98, "w": 3.04, "x": 1.89, "y": 1.98, "z": 1.80, "{": 1.28, "|": 1.02, "}": 1.28, "~": 2.10 }, "height": 4.23 }} ================================================ FILE: deps/mkfile ================================================ # Mkfile for updating the glyphsize table. This is not intended for general use. # If you really want to use it, you'll probably have to change FONTS to point to # the font files you want to serialize, then obtain a copy of mk, either from # plan9port, or from github.com/dcjones/mk. Then just type 'mk'. all:V: glyphsize.json FONTS=/System/Library/Fonts/HelveticaNeue.dfont \ /System/Library/Fonts/Helvetica.dfont \ /Library/Fonts/Microsoft/Arial.ttf \ /Library/Fonts/PTSans.ttc \ /Users/dcjones/Library/Fonts/SourceSansPro-Regular.otf glyphsize.json: glyphsize $FONTS ./glyphsize $FONTS > $target CFLAGS=-Wall -g -I/usr/local/include/freetype2 glyphsize: glyphsize.c gcc $CFLAGS -o $target $prereq -lfreetype clean:V: rm -f glyphsize # Uglifyjs outputs a bunch of non-ascii characters that cause problems # when embedded in an svg file, so use closure-complier. snap.svg-min.js: /Users/dcjones/src/Snap.svg-0.3.0/dist/snap.svg.js closure-compiler $prereq > $target ================================================ FILE: deps/snap.svg-min.js ================================================ (function(N){var k=/[\.\/]/,L=/\s*,\s*/,C=function(a,d){return a-d},a,v,y={n:{}},M=function(){for(var a=0,d=this.length;au[s].zIndex&& (e[u[s].zIndex]=u[s]));for(q.sort(C);0>q[p];)if(b=e[q[p++]],l.push(b.apply(d,n)),v)return v=f,l;for(s=0;sa?-1:1);b=-e-b;b=Math.pow(Math.abs(b),1/3)*(0>b?-1:1);a=a+b+0.5;return 3*(1-a)*a*a+a*a*a};e.backin=function(a){return 1==a?1:a*a*(2.70158*a-1.70158)};e.backout=function(a){if(0==a)return 0;a-=1;return a*a*(2.70158*a+1.70158)+1};e.elastic=function(a){return a==!!a?a:Math.pow(2,-10*a)*Math.sin(2*(a-0.075)*Math.PI/0.3)+1};e.bounce=function(a){a<1/2.75?a*=7.5625*a:a<2/2.75?(a-=1.5/2.75,a=7.5625*a*a+0.75):a<2.5/2.75?(a-=2.25/2.75,a=7.5625*a*a+0.9375):(a-=2.625/2.75,a=7.5625*a*a+0.984375);return a}; return N.mina=e}("undefined"==typeof k?function(){}:k),C=function(){function a(c,t){if(c){if(c.tagName)return x(c);if(y(c,"array")&&a.set)return a.set.apply(a,c);if(c instanceof e)return c;if(null==t)return c=G.doc.querySelector(c),x(c)}return new s(null==c?"100%":c,null==t?"100%":t)}function v(c,a){if(a){"#text"==c&&(c=G.doc.createTextNode(a.text||""));"string"==typeof c&&(c=v(c));if("string"==typeof a)return"xlink:"==a.substring(0,6)?c.getAttributeNS(m,a.substring(6)):"xml:"==a.substring(0,4)?c.getAttributeNS(la, a.substring(4)):c.getAttribute(a);for(var da in a)if(a[h](da)){var b=J(a[da]);b?"xlink:"==da.substring(0,6)?c.setAttributeNS(m,da.substring(6),b):"xml:"==da.substring(0,4)?c.setAttributeNS(la,da.substring(4),b):c.setAttribute(da,b):c.removeAttribute(da)}}else c=G.doc.createElementNS(la,c);return c}function y(c,a){a=J.prototype.toLowerCase.call(a);return"finite"==a?isFinite(c):"array"==a&&(c instanceof Array||Array.isArray&&Array.isArray(c))?!0:"null"==a&&null===c||a==typeof c&&null!==c||"object"== a&&c===Object(c)||$.call(c).slice(8,-1).toLowerCase()==a}function M(c){if("function"==typeof c||Object(c)!==c)return c;var a=new c.constructor,b;for(b in c)c[h](b)&&(a[b]=M(c[b]));return a}function A(c,a,b){function m(){var e=Array.prototype.slice.call(arguments,0),f=e.join("\u2400"),d=m.cache=m.cache||{},l=m.count=m.count||[];if(d[h](f)){a:for(var e=l,l=f,B=0,H=e.length;Bc-b)return a-m+c}return a};a.getRGB=A(function(c){if(!c||(c=J(c)).indexOf("-")+1)return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:ka};if("none"==c)return{r:-1,g:-1,b:-1,hex:"none",toString:ka};!X[h](c.toLowerCase().substring(0, 2))&&"#"!=c.charAt()&&(c=T(c));if(!c)return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:ka};var b,m,e,f,d;if(c=c.match(F)){c[2]&&(e=U(c[2].substring(5),16),m=U(c[2].substring(3,5),16),b=U(c[2].substring(1,3),16));c[3]&&(e=U((d=c[3].charAt(3))+d,16),m=U((d=c[3].charAt(2))+d,16),b=U((d=c[3].charAt(1))+d,16));c[4]&&(d=c[4].split(S),b=K(d[0]),"%"==d[0].slice(-1)&&(b*=2.55),m=K(d[1]),"%"==d[1].slice(-1)&&(m*=2.55),e=K(d[2]),"%"==d[2].slice(-1)&&(e*=2.55),"rgba"==c[1].toLowerCase().slice(0,4)&&(f=K(d[3])), d[3]&&"%"==d[3].slice(-1)&&(f/=100));if(c[5])return d=c[5].split(S),b=K(d[0]),"%"==d[0].slice(-1)&&(b/=100),m=K(d[1]),"%"==d[1].slice(-1)&&(m/=100),e=K(d[2]),"%"==d[2].slice(-1)&&(e/=100),"deg"!=d[0].slice(-3)&&"\u00b0"!=d[0].slice(-1)||(b/=360),"hsba"==c[1].toLowerCase().slice(0,4)&&(f=K(d[3])),d[3]&&"%"==d[3].slice(-1)&&(f/=100),a.hsb2rgb(b,m,e,f);if(c[6])return d=c[6].split(S),b=K(d[0]),"%"==d[0].slice(-1)&&(b/=100),m=K(d[1]),"%"==d[1].slice(-1)&&(m/=100),e=K(d[2]),"%"==d[2].slice(-1)&&(e/=100), "deg"!=d[0].slice(-3)&&"\u00b0"!=d[0].slice(-1)||(b/=360),"hsla"==c[1].toLowerCase().slice(0,4)&&(f=K(d[3])),d[3]&&"%"==d[3].slice(-1)&&(f/=100),a.hsl2rgb(b,m,e,f);b=Q(I.round(b),255);m=Q(I.round(m),255);e=Q(I.round(e),255);f=Q(P(f,0),1);c={r:b,g:m,b:e,toString:ka};c.hex="#"+(16777216|e|m<<8|b<<16).toString(16).slice(1);c.opacity=y(f,"finite")?f:1;return c}return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:ka}},a);a.hsb=A(function(c,b,m){return a.hsb2rgb(c,b,m).hex});a.hsl=A(function(c,b,m){return a.hsl2rgb(c, b,m).hex});a.rgb=A(function(c,a,b,m){if(y(m,"finite")){var e=I.round;return"rgba("+[e(c),e(a),e(b),+m.toFixed(2)]+")"}return"#"+(16777216|b|a<<8|c<<16).toString(16).slice(1)});var T=function(c){var a=G.doc.getElementsByTagName("head")[0]||G.doc.getElementsByTagName("svg")[0];T=A(function(c){if("red"==c.toLowerCase())return"rgb(255, 0, 0)";a.style.color="rgb(255, 0, 0)";a.style.color=c;c=G.doc.defaultView.getComputedStyle(a,aa).getPropertyValue("color");return"rgb(255, 0, 0)"==c?null:c});return T(c)}, qa=function(){return"hsb("+[this.h,this.s,this.b]+")"},ra=function(){return"hsl("+[this.h,this.s,this.l]+")"},ka=function(){return 1==this.opacity||null==this.opacity?this.hex:"rgba("+[this.r,this.g,this.b,this.opacity]+")"},D=function(c,b,m){null==b&&y(c,"object")&&"r"in c&&"g"in c&&"b"in c&&(m=c.b,b=c.g,c=c.r);null==b&&y(c,string)&&(m=a.getRGB(c),c=m.r,b=m.g,m=m.b);if(1b?b:1-b);a=d*(1-Y(c%2-1));b=e= h=b-d/2;c=~~c;b+=[d,a,0,0,a,d][c];e+=[a,d,d,a,0,0][c];h+=[0,0,a,d,d,a][c];return oa(b,e,h,m)};a.rgb2hsb=function(c,a,b){b=D(c,a,b);c=b[0];a=b[1];b=b[2];var m,e;m=P(c,a,b);e=m-Q(c,a,b);c=((0==e?0:m==c?(a-b)/e:m==a?(b-c)/e+2:(c-a)/e+4)+360)%6*60/360;return{h:c,s:0==e?0:e/m,b:m,toString:qa}};a.rgb2hsl=function(c,a,b){b=D(c,a,b);c=b[0];a=b[1];b=b[2];var m,e,h;m=P(c,a,b);e=Q(c,a,b);h=m-e;c=((0==h?0:m==c?(a-b)/h:m==a?(b-c)/h+2:(c-a)/h+4)+360)%6*60/360;m=(m+e)/2;return{h:c,s:0==h?0:0.5>m?h/(2*m):h/(2-2* m),l:m,toString:ra}};a.parsePathString=function(c){if(!c)return null;var b=a.path(c);if(b.arr)return a.path.clone(b.arr);var m={a:7,c:6,o:2,h:1,l:2,m:2,r:4,q:4,s:4,t:2,v:1,u:3,z:0},e=[];y(c,"array")&&y(c[0],"array")&&(e=a.path.clone(c));e.length||J(c).replace(W,function(c,a,b){var h=[];c=a.toLowerCase();b.replace(Z,function(c,a){a&&h.push(+a)});"m"==c&&2= m[c]&&(e.push([a].concat(h.splice(0,m[c]))),m[c]););});e.toString=a.path.toString;b.arr=a.path.clone(e);return e};var O=a.parseTransformString=function(c){if(!c)return null;var b=[];y(c,"array")&&y(c[0],"array")&&(b=a.path.clone(c));b.length||J(c).replace(ma,function(c,a,m){var e=[];a.toLowerCase();m.replace(Z,function(c,a){a&&e.push(+a)});b.push([a].concat(e))});b.toString=a.path.toString;return b};a._.svgTransform2string=d;a._.rgTransform=RegExp("^[a-z][\t\n\x0B\f\r \u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*-?\\.?\\d", "i");a._.transform2matrix=f;a._unit2px=b;a._.getSomeDefs=u;a._.getSomeSVG=p;a.select=function(c){return x(G.doc.querySelector(c))};a.selectAll=function(c){c=G.doc.querySelectorAll(c);for(var b=(a.set||Array)(),m=0;m")}else c&&(a+="/>");return a}}c.attr=function(c,a){if(!c)return this;if(y(c,"string"))if(1)/)||(c=""+c+"",b=!1);m.innerHTML=c;if(c=m.getElementsByTagName("svg")[0])if(b)a=c;else for(;c.firstChild;)a.appendChild(c.firstChild);m.innerHTML=aa;return new l(a)};l.prototype.select=e.prototype.select;l.prototype.selectAll=e.prototype.selectAll;a.fragment=function(){for(var c=Array.prototype.slice.call(arguments,0),b=G.doc.createDocumentFragment(),m=0,e=c.length;ma;a++)for(e=0;3>e;e++){for(f=n=0;3>f;f++)n+=u[a][f]*d[f][e];k[a][e]=n}this.a=k[0][0];this.b=k[1][0];this.c=k[0][1];this.d=k[1][1];this.e=k[0][2];this.f=k[1][2];return this};n.invert=function(){var a=this.a*this.d-this.b*this.c;return new w(this.d/a,-this.b/a,-this.c/a,this.a/a,(this.c*this.f-this.d*this.e)/a,(this.b*this.e-this.a*this.f)/a)};n.clone=function(){return new w(this.a,this.b,this.c,this.d,this.e, this.f)};n.translate=function(a,d){return this.add(1,0,0,1,a,d)};n.scale=function(a,d,e,f){null==d&&(d=a);(e||f)&&this.add(1,0,0,1,e,f);this.add(a,0,0,d,0,0);(e||f)&&this.add(1,0,0,1,-e,-f);return this};n.rotate=function(b,d,e){b=a.rad(b);d=d||0;e=e||0;var l=+f.cos(b).toFixed(9);b=+f.sin(b).toFixed(9);this.add(l,b,-b,l,d,e);return this.add(1,0,0,1,-d,-e)};n.x=function(a,d){return a*this.a+d*this.c+this.e};n.y=function(a,d){return a*this.b+d*this.d+this.f};n.get=function(a){return+this[d.fromCharCode(97+ a)].toFixed(4)};n.toString=function(){return"matrix("+[this.get(0),this.get(1),this.get(2),this.get(3),this.get(4),this.get(5)].join()+")"};n.offset=function(){return[this.e.toFixed(4),this.f.toFixed(4)]};n.determinant=function(){return this.a*this.d-this.b*this.c};n.split=function(){var b={};b.dx=this.e;b.dy=this.f;var d=[[this.a,this.c],[this.b,this.d]];b.scalex=f.sqrt(k(d[0]));p(d[0]);b.shear=d[0][0]*d[1][0]+d[0][1]*d[1][1];d[1]=[d[1][0]-d[0][0]*b.shear,d[1][1]-d[0][1]*b.shear];b.scaley=f.sqrt(k(d[1])); p(d[1]);b.shear/=b.scaley;0>this.determinant()&&(b.scalex=-b.scalex);var e=-d[0][1],d=d[1][1];0>d?(b.rotate=a.deg(f.acos(d)),0>e&&(b.rotate=360-b.rotate)):b.rotate=a.deg(f.asin(e));b.isSimple=!+b.shear.toFixed(9)&&(b.scalex.toFixed(9)==b.scaley.toFixed(9)||!b.rotate);b.isSuperSimple=!+b.shear.toFixed(9)&&b.scalex.toFixed(9)==b.scaley.toFixed(9)&&!b.rotate;b.noRotation=!+b.shear.toFixed(9)&&!b.rotate;return b};n.toTransformString=function(a){a=a||this.split();if(+a.shear.toFixed(9))return"m"+[this.get(0), this.get(1),this.get(2),this.get(3),this.get(4),this.get(5)];a.scalex=+a.scalex.toFixed(4);a.scaley=+a.scaley.toFixed(4);a.rotate=+a.rotate.toFixed(4);return(a.dx||a.dy?"t"+[+a.dx.toFixed(4),+a.dy.toFixed(4)]:"")+(1!=a.scalex||1!=a.scaley?"s"+[a.scalex,a.scaley,0,0]:"")+(a.rotate?"r"+[+a.rotate.toFixed(4),0,0]:"")}})(w.prototype);a.Matrix=w;a.matrix=function(a,d,f,b,k,e){return new w(a,d,f,b,k,e)}});C.plugin(function(a,v,y,M,A){function w(h){return function(d){k.stop();d instanceof A&&1==d.node.childNodes.length&& ("radialGradient"==d.node.firstChild.tagName||"linearGradient"==d.node.firstChild.tagName||"pattern"==d.node.firstChild.tagName)&&(d=d.node.firstChild,b(this).appendChild(d),d=u(d));if(d instanceof v)if("radialGradient"==d.type||"linearGradient"==d.type||"pattern"==d.type){d.node.id||e(d.node,{id:d.id});var f=l(d.node.id)}else f=d.attr(h);else f=a.color(d),f.error?(f=a(b(this).ownerSVGElement).gradient(d))?(f.node.id||e(f.node,{id:f.id}),f=l(f.node.id)):f=d:f=r(f);d={};d[h]=f;e(this.node,d);this.node.style[h]= x}}function z(a){k.stop();a==+a&&(a+="px");this.node.style.fontSize=a}function d(a){var b=[];a=a.childNodes;for(var e=0,f=a.length;eb.opacity&&(k["stop-opacity"]=b.opacity);e(f,k);this.node.appendChild(f);return this}function u(){if("linearGradient"==this.type){var b=e(this.node,"x1")||0,d=e(this.node,"x2")|| 1,f=e(this.node,"y1")||0,k=e(this.node,"y2")||0;return a._.box(b,f,math.abs(d-b),math.abs(k-f))}b=this.node.r||0;return a._.box((this.node.cx||0.5)-b,(this.node.cy||0.5)-b,2*b,2*b)}function p(a,d){function f(a,b){for(var d=(b-u)/(a-w),e=w;ep||e(a,b,d,h,f,k,l,n)h){if(d&&!s.start){n=f(n,p,D[1],D[2],D[3],D[4],D[5],D[6],h-c);O+=["C"+e(n.start.x),e(n.start.y),e(n.m.x),e(n.m.y),e(n.x),e(n.y)];if(l)return O;s.start=O;O=["M"+e(n.x),e(n.y)+"C"+e(n.n.x),e(n.n.y),e(n.end.x),e(n.end.y),e(D[5]),e(D[6])].join();c+=q;n=+D[5];p=+D[6];continue}if(!b&&!d)return n=f(n,p,D[1],D[2],D[3],D[4],D[5],D[6],h-c)}c+=q;n=+D[5];p=+D[6]}O+= D.shift()+D}s.end=O;return n=b?c:d?s:u(n,p,D[0],D[1],D[2],D[3],D[4],D[5],1)},null,a._.clone)}function u(a,b,d,e,h,f,k,l,n){var p=1-n,q=ma(p,3),s=ma(p,2),c=n*n,t=c*n,r=q*a+3*s*n*d+3*p*n*n*h+t*k,q=q*b+3*s*n*e+3*p*n*n*f+t*l,s=a+2*n*(d-a)+c*(h-2*d+a),t=b+2*n*(e-b)+c*(f-2*e+b),x=d+2*n*(h-d)+c*(k-2*h+d),c=e+2*n*(f-e)+c*(l-2*f+e);a=p*a+n*d;b=p*b+n*e;h=p*h+n*k;f=p*f+n*l;l=90-180*F.atan2(s-x,t-c)/S;return{x:r,y:q,m:{x:s,y:t},n:{x:x,y:c},start:{x:a,y:b},end:{x:h,y:f},alpha:l}}function p(b,d,e,h,f,n,k,l){a.is(b, "array")||(b=[b,d,e,h,f,n,k,l]);b=U.apply(null,b);return w(b.min.x,b.min.y,b.max.x-b.min.x,b.max.y-b.min.y)}function b(a,b,d){return b>=a.x&&b<=a.x+a.width&&d>=a.y&&d<=a.y+a.height}function q(a,d){a=w(a);d=w(d);return b(d,a.x,a.y)||b(d,a.x2,a.y)||b(d,a.x,a.y2)||b(d,a.x2,a.y2)||b(a,d.x,d.y)||b(a,d.x2,d.y)||b(a,d.x,d.y2)||b(a,d.x2,d.y2)||(a.xd.x||d.xa.x)&&(a.yd.y||d.ya.y)}function e(a,b,d,e,h,f,n,k,l){null==l&&(l=1);l=(1l?0:l)/2;for(var p=[-0.1252, 0.1252,-0.3678,0.3678,-0.5873,0.5873,-0.7699,0.7699,-0.9041,0.9041,-0.9816,0.9816],q=[0.2491,0.2491,0.2335,0.2335,0.2032,0.2032,0.1601,0.1601,0.1069,0.1069,0.0472,0.0472],s=0,c=0;12>c;c++)var t=l*p[c]+l,r=t*(t*(-3*a+9*d-9*h+3*n)+6*a-12*d+6*h)-3*a+3*d,t=t*(t*(-3*b+9*e-9*f+3*k)+6*b-12*e+6*f)-3*b+3*e,s=s+q[c]*F.sqrt(r*r+t*t);return l*s}function l(a,b,d){a=I(a);b=I(b);for(var h,f,l,n,k,s,r,O,x,c,t=d?0:[],w=0,v=a.length;wZ(L.x-Q.x)?"y":"x",S=0.001>Z(C.x-B.x)?"y":"x",R;R=Q.x;var Y=Q.y,V=L.x,ea=L.y,fa=B.x,ga=B.y,ha=C.x,ia=C.y;if(W(R,V)W(fa,ha)||W(Y,ea)W(ga,ia))R=void 0;else{var $=(R*ea-Y*V)*(fa-ha)-(R-V)*(fa*ia-ga*ha),aa=(R*ea-Y*V)*(ga-ia)-(Y-ea)*(fa*ia-ga*ha),ja=(R-V)*(ga-ia)-(Y-ea)*(fa-ha);if(ja){var $=$/ja,aa=aa/ja,ja=+$.toFixed(2),ba=+aa.toFixed(2);R=ja<+X(R,V).toFixed(2)||ja>+W(R,V).toFixed(2)||ja<+X(fa,ha).toFixed(2)|| ja>+W(fa,ha).toFixed(2)||ba<+X(Y,ea).toFixed(2)||ba>+W(Y,ea).toFixed(2)||ba<+X(ga,ia).toFixed(2)||ba>+W(ga,ia).toFixed(2)?void 0:{x:$,y:aa}}else R=void 0}R&&F[R.x.toFixed(4)]!=R.y.toFixed(4)&&(F[R.x.toFixed(4)]=R.y.toFixed(4),Q=Q.t+Z((R[N]-Q[N])/(L[N]-Q[N]))*(L.t-Q.t),B=B.t+Z((R[S]-B[S])/(C[S]-B[S]))*(C.t-B.t),0<=Q&&1>=Q&&0<=B&&1>=B&&(z?M++:M.push({x:R.x,y:R.y,t1:Q,t2:B})))}z=M}else z=z?0:[];if(d)t+=z;else{H=0;for(J=z.length;Hv&&(v=2*S+v);0>t&&(t=2*S+t);n&&v>t&&(v-=2*S);!n&&t>v&&(t-=2*S)}if(Z(t-v)>r){var c=t,w=k,G=p;t=v+r*(n&&t>v?1:-1);k=l+e*F.cos(t);p=u+h*F.sin(t);c=K(k,p,e,h,f,0,n,w,G,[t,c,l,u])}l=t-v;f=F.cos(v);r=F.sin(v);n=F.cos(t);t=F.sin(t);l=F.tan(l/4);e=4/3*e*l;l*=4/3*h;h=[b,d];b=[b+e*r,d-l*f];d=[k+e*t,p-l*n];k=[k,p];b[0]=2*h[0]-b[0];b[1]=2*h[1]-b[1];if(s)return[b,d,k].concat(c); c=[b,d,k].concat(c).join().split(",");s=[];k=0;for(p=c.length;kq;++q)0==q?(r=6*a-12*d+6*h,s=-3*a+9*d-9*h+3*l,c=3*d-3*a):(r=6*b-12*e+6*f,s=-3*b+9*e-9*f+3*k,c=3*e-3*b),1E-12>Z(s)?1E-12>Z(r)||(s=-c/r,0s&&n.push(s)):(t=r*r-4*c*s,c=F.sqrt(t),0>t||(t=(-r+c)/(2*s),0t&&n.push(t),s=(-r-c)/(2*s),0s&&n.push(s)));for(r=q=n.length;q--;)s=n[q],c=1-s,p[0][q]=c*c*c*a+3* c*c*s*d+3*c*s*s*h+s*s*s*l,p[1][q]=c*c*c*b+3*c*c*s*e+3*c*s*s*f+s*s*s*k;p[0][r]=a;p[1][r]=b;p[0][r+1]=l;p[1][r+1]=k;p[0].length=p[1].length=r+2;return{min:{x:X.apply(0,p[0]),y:X.apply(0,p[1])},max:{x:W.apply(0,p[0]),y:W.apply(0,p[1])}}}function I(a,b){var e=!b&&A(a);if(!b&&e.curve)return d(e.curve);var f=G(a),l=b&&G(b),n={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},k={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},p=function(a,b,c){if(!a)return["C",b.x,b.y,b.x,b.y,b.x,b.y];a[0]in{T:1,Q:1}||(b.qx=b.qy=null); switch(a[0]){case "M":b.X=a[1];b.Y=a[2];break;case "A":a=["C"].concat(K.apply(0,[b.x,b.y].concat(a.slice(1))));break;case "S":"C"==c||"S"==c?(c=2*b.x-b.bx,b=2*b.y-b.by):(c=b.x,b=b.y);a=["C",c,b].concat(a.slice(1));break;case "T":"Q"==c||"T"==c?(b.qx=2*b.x-b.qx,b.qy=2*b.y-b.qy):(b.qx=b.x,b.qy=b.y);a=["C"].concat(J(b.x,b.y,b.qx,b.qy,a[1],a[2]));break;case "Q":b.qx=a[1];b.qy=a[2];a=["C"].concat(J(b.x,b.y,a[1],a[2],a[3],a[4]));break;case "L":a=["C"].concat(h(b.x,b.y,a[1],a[2]));break;case "H":a=["C"].concat(h(b.x, b.y,a[1],b.y));break;case "V":a=["C"].concat(h(b.x,b.y,b.x,a[1]));break;case "Z":a=["C"].concat(h(b.x,b.y,b.X,b.Y))}return a},s=function(a,b){if(7e;e+=2){var f=[{x:+a[e-2],y:+a[e-1]},{x:+a[e],y:+a[e+1]},{x:+a[e+2],y:+a[e+3]},{x:+a[e+4],y:+a[e+5]}];b?e?h-4==e?f[3]={x:+a[0],y:+a[1]}:h-2==e&&(f[2]={x:+a[0],y:+a[1]},f[3]={x:+a[2],y:+a[3]}):f[0]={x:+a[h-2],y:+a[h-1]}:h-4==e?f[3]=f[2]:e||(f[0]={x:+a[e],y:+a[e+1]});d.push(["C",(-f[0].x+6*f[1].x+f[2].x)/6,(-f[0].y+6*f[1].y+f[2].y)/6,(f[1].x+6*f[2].x-f[3].x)/6,(f[1].y+6*f[2].y-f[3].y)/6,f[2].x,f[2].y])}return d}y=k.prototype;var Q=a.is,C=a._.clone,L="hasOwnProperty", N=/,?([a-z]),?/gi,$=parseFloat,F=Math,S=F.PI,X=F.min,W=F.max,ma=F.pow,Z=F.abs;M=n(1);var na=n(),ba=n(0,1),V=a._unit2px;a.path=A;a.path.getTotalLength=M;a.path.getPointAtLength=na;a.path.getSubpath=function(a,b,d){if(1E-6>this.getTotalLength(a)-d)return ba(a,b).end;a=ba(a,d,1);return b?ba(a,b).end:a};y.getTotalLength=function(){if(this.node.getTotalLength)return this.node.getTotalLength()};y.getPointAtLength=function(a){return na(this.attr("d"),a)};y.getSubpath=function(b,d){return a.path.getSubpath(this.attr("d"), b,d)};a._.box=w;a.path.findDotsAtSegment=u;a.path.bezierBBox=p;a.path.isPointInsideBBox=b;a.path.isBBoxIntersect=q;a.path.intersection=function(a,b){return l(a,b)};a.path.intersectionNumber=function(a,b){return l(a,b,1)};a.path.isPointInside=function(a,d,e){var h=r(a);return b(h,d,e)&&1==l(a,[["M",d,e],["H",h.x2+10]],1)%2};a.path.getBBox=r;a.path.get={path:function(a){return a.attr("path")},circle:function(a){a=V(a);return x(a.cx,a.cy,a.r)},ellipse:function(a){a=V(a);return x(a.cx||0,a.cy||0,a.rx, a.ry)},rect:function(a){a=V(a);return s(a.x||0,a.y||0,a.width,a.height,a.rx,a.ry)},image:function(a){a=V(a);return s(a.x||0,a.y||0,a.width,a.height)},line:function(a){return"M"+[a.attr("x1")||0,a.attr("y1")||0,a.attr("x2"),a.attr("y2")]},polyline:function(a){return"M"+a.attr("points")},polygon:function(a){return"M"+a.attr("points")+"z"},deflt:function(a){a=a.node.getBBox();return s(a.x,a.y,a.width,a.height)}};a.path.toRelative=function(b){var e=A(b),h=String.prototype.toLowerCase;if(e.rel)return d(e.rel); a.is(b,"array")&&a.is(b&&b[0],"array")||(b=a.parsePathString(b));var f=[],l=0,n=0,k=0,p=0,s=0;"M"==b[0][0]&&(l=b[0][1],n=b[0][2],k=l,p=n,s++,f.push(["M",l,n]));for(var r=b.length;sa?A(this.length+a,0):a;f=A(0,w(this.length-a,f));var u=[],p=[],b=[],q;for(q=2;q',{def:null==f?d:[d,f]})};a.filter.blur.toString=function(){return this()};a.filter.shadow= function(d,f,k,u,p){"string"==typeof k&&(p=u=k,k=4);"string"!=typeof u&&(p=u,u="#000");null==k&&(k=4);null==p&&(p=1);null==d&&(d=0,f=2);null==f&&(f=d);u=a.color(u||"#000");return a.format('', {color:u,dx:d,dy:f,blur:k,opacity:p})};a.filter.shadow.toString=function(){return this()};a.filter.grayscale=function(d){null==d&&(d=1);return a.format('',{a:0.2126+0.7874*(1-d),b:0.7152-0.7152*(1-d),c:0.0722-0.0722*(1-d),d:0.2126-0.2126*(1-d),e:0.7152+0.2848*(1-d),f:0.0722-0.0722*(1-d),g:0.2126-0.2126*(1-d),h:0.0722+0.9278*(1-d)})};a.filter.grayscale.toString=function(){return this()};a.filter.sepia= function(d){null==d&&(d=1);return a.format('',{a:0.393+0.607*(1-d),b:0.769-0.769*(1-d),c:0.189-0.189*(1-d),d:0.349-0.349*(1-d),e:0.686+0.314*(1-d),f:0.168-0.168*(1-d),g:0.272-0.272*(1-d),h:0.534-0.534*(1-d),i:0.131+0.869*(1-d)})};a.filter.sepia.toString=function(){return this()};a.filter.saturate=function(d){null==d&&(d=1);return a.format('',{amount:1- d})};a.filter.saturate.toString=function(){return this()};a.filter.hueRotate=function(d){return a.format('',{angle:d||0})};a.filter.hueRotate.toString=function(){return this()};a.filter.invert=function(d){null==d&&(d=1);return a.format('',{amount:d, amount2:1-d})};a.filter.invert.toString=function(){return this()};a.filter.brightness=function(d){null==d&&(d=1);return a.format('',{amount:d})};a.filter.brightness.toString=function(){return this()};a.filter.contrast=function(d){null==d&&(d=1);return a.format('', {amount:d,amount2:0.5-d/2})};a.filter.contrast.toString=function(){return this()}});return C}); ================================================ FILE: docs/Project.toml ================================================ [deps] Cairo = "159f3aea-2a34-519c-b102-8c37f9878175" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" Compose = "a81c6b42-2e10-5240-aca2-a61377ecd94b" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Fontconfig = "186bb1d3-e1f7-5a2c-a377-96d770f13627" Measures = "442fdcdd-2543-5da2-b0f3-8c86c306513e" ================================================ FILE: docs/make.jl ================================================ using Documenter, Compose import Cairo struct SVGJSWritable{T} x :: T end Base.show(io::IO, m::MIME"text/html", x::SVGJSWritable) = show(io, m, x.x) makedocs( modules = [Compose], clean = false, sitename = "Compose.jl", pages = Any[ "Home" => "index.md", "Tutorial" => "tutorial.md", "Gallery" => Any[ "Forms" => "gallery/forms.md", "Properties"=> "gallery/properties.md", "Transformations"=> "gallery/transforms.md", ], "Library" => "library.md" ] ) deploydocs( repo = "github.com/GiovineItalia/Compose.jl.git", ) ================================================ FILE: docs/src/gallery/forms.md ================================================ ```@meta Author = ["Mattriks"] ``` # [Forms](@id forms_gallery) ## [`arc`](@ref) ```@example using Compose set_default_graphic_size(8cm,4cm) colv = ["red","orange","green","blue", "purple"] a = range(-π/4, stop=7π/4, length=6)+ 0.2*randn(6) a[6] = a[1] img1 = compose(context(), (context(), arc([0.5], [0.5], [0.3], [4.5π/4,π/4] , [7.5π/4,3π/4], [true,false]), stroke("black"), fill(["red","white"])) ) img2 = compose(context(), (context(), arc([0.5], [0.5], [0.3], a[1:5], a[2:6]), stroke(colv), fill("transparent"), linewidth(2mm)) ) hstack(img1, img2) ``` ## [`bezigon`](@ref) ```@example using Colors, Compose set_default_graphic_size(14cm,10cm) petal = [[(0.4, 0.4), (0.4, 0.2), (0.5, 0.0)], [(0.6, 0.2), (0.6, 0.4), (0.5, 0.5)]] petalf(θ::Float64) = (context(rotation=Rotation(θ, 0.5,0.5)), bezigon((0.5, 0.5), petal), fill(LCHuvA(70.,50., 360*θ/2π, 0.4))) theta = range(π/20, 2π, step=2π/10).-π img = compose(context(), petalf.(theta)...) ``` ## [`bitmap`](@ref) ```@example using Main: SVGJSWritable #hide using Compose set_default_graphic_size(14cm,4cm) rawimg = read(joinpath(@__DIR__,"..","assets/smiley.png")); X = 0.9*rand(10,2) img = compose(context(), (context(), rectangle(), fill("transparent"), stroke("orange")), (context(), bitmap(["image/png"], [rawimg], X[:,1], X[:,2], [0.1], [0.1])) ) SVGJSWritable(ans) #hide ``` ## [`circle`](@ref) ```@example using Colors, Compose set_default_graphic_size(14cm,4cm) colv = HSVA.([0:30:179;], 1, 1, 0.5) img = compose(context(units=UnitBox(0,0,40,8)), (context(), circle([5.0:6:35;], [4], [4]), fill(colv), stroke("black")) ) ``` ## [`curve`](@ref) ```@example using Colors, Compose set_default_graphic_size(14cm, 4cm) epoint(x) = [(x,y) for y in rand(10)] cpoint(t=0) = [(t+x,y) for (x,y) in zip(rand(10), 0.5*rand(10))] colv = range(colorant"blue",stop=colorant"orange", length=10) img = compose(context(units=UnitBox(0,0,2,1)), (context(), curve([(0.5,1.0)], cpoint(), cpoint(), epoint(0.5)), stroke(colv)), (context(), curve([(1.5,1.0)], cpoint(1), cpoint(1), epoint(1.5)), stroke(colv)) ) ``` ## [`ellipse`](@ref) ```@example using Colors, Compose set_default_graphic_size(14cm, 4cm) colv1 = HSVA.([0:30:179;], 1, 1, 0.5) r = 2*[1:6;]/24 colv2 = HSVA.([0:15:179;], 1, 1, 0.3) θ = collect(range(0, stop=1.9π, length=10)) rl = 0.5*rand(10) rw = 0.3*rand(10) rot = Rotation.(θ, 1.5, 0.5) ellipsef(i::Int) = ellipse(1.5, 0.5, rl[i], rw[i]) img = compose(context(units=UnitBox(0,0,2,1)), (context(), ellipse(r,[0.5],r,reverse(r)), stroke("black"), fill(colv1)), [(context(rotation=rot[i]), ellipsef(i), fill(colv2[i]), stroke("black")) for i in 1:10]... ) ``` ## [`line`](@ref) ```@example using Compose set_default_graphic_size(10cm, 10cm) θ = collect(range(0, stop=2π, length=60)) point_array = [[(0,0.75), (x,y)] for (x,y) in zip(cos.(θ), sin.(θ))] img = compose(context(), (context(), rectangle(), fill("salmon"), fillopacity(0.3)), (context(0.12, 0.12, 0.76, 0.76, units=UnitBox(-1,-1,2,2)), line(point_array), stroke("gold"), linewidth(1mm)) ) ``` ## [`ngon`](@ref), [`star`](@ref), [`xgon`](@ref) ```@example using Compose set_default_graphic_size(14cm, 5cm) rainbow = ["orange","green","indigo", "darkviolet","indigo","blue","green","yellow","orange","red"] properties = [fillopacity(0.5), fill(rainbow), stroke("black")] npoints = [7,5,3,2,3,4,5,6,7,8] X = range(0.06, stop=0.94, length=10) radii = 0.035*[-ones(3); ones(7)] p = compose(context(), (context(), ngon(X, [0.16], radii, npoints), star(X, [0.5], radii, npoints), xgon(X, [0.84], radii, npoints), properties...)) ``` ## [`polygon`](@ref) ```@example using Statistics, Compose set_default_graphic_size(10cm,10cm) X = randn(50,2) X = 0.3*(X .- mean(X,dims=1))./std(X,dims=1) hp = hypot.(X[:,1],X[:,2]) i = hp .> Statistics.quantile(hp, 0.82) Z = X[i,:] θ = atan.(Z[:,1], Z[:,2]) ord = sortperm(θ) polypoints = [(x,y) for (x,y) in zip(Z[ord,1],Z[ord,2])] img = compose(context(units=UnitBox(-1,-1, 2,2)), (context(), line([(-1,-1), (-1,1), (1,1)]), stroke("black")), (context(), circle(0,0,0.02), fill("red")), (context(), circle(X[:,1],X[:,2],[0.02]), fill("transparent"),stroke("deepskyblue")), (context(), polygon(polypoints), fill("red"), fillopacity(0.1)) ) ``` ## [`rectangle`](@ref) ```@example using Colors, Compose set_default_graphic_size(14cm,4cm) colv = HSVA.([0:15:179;], 1, 1, 0.3) X = 0.9*rand(10,2) rl = 0.3*rand(10).+0.03 rw = 0.3*rand(10).+0.03 img = compose(context(), (context(), rectangle(), fill("transparent"), stroke("orange")), (context(), rectangle(X[:,1], X[:,2], rl, rw), fill(colv), stroke("black")) ) ``` ## [`sector`](@ref) ```@example using Compose set_default_graphic_size(14cm, 4cm) colv = ["red","orange","green","blue", "purple"] a = range(-π/4, stop=7π/4, length=6)+ 0.2*randn(6) a[6] = a[1] sectorobj = sector([0.5], [0.5], [0.3], a[1:5], a[2:6]) img1 = compose(context(), (context(), sectorobj, fill(colv)) ) img2 = compose(context(), (context(), sectorobj, stroke("white"), fill(colv), linewidth(1.4mm)) ) img3 = compose(context(), (context(), sectorobj, stroke(colv), fill("transparent"), linewidth(1.4mm)) ) hstack(img1, img2, img3) ``` ## [`text`](@ref) ```@example using Colors, Compose set_default_graphic_size(10cm,10cm) labels=rand(string.(names(Base)[280:end]), 30) θ = collect(range(0, stop=58π/30, length=30)) X = 1 .+ 0.7*[cos.(θ) sin.(θ)] colv = range(colorant"blue",stop=colorant"orange", length=30) rot = Rotation.(θ, X[:,1], X[:,2]) img = compose(context(units=UnitBox(0,0,2,2)), (context(), text(1, 1, "Julia", hcenter, vcenter), stroke("red"), fontsize(30pt)), (context(), text(X[:,1], X[:,2], labels, [hcenter], [vcenter], rot), stroke(colv)) ) ``` ```@example using Compose set_default_graphic_size(10cm,8cm) # This graphic illustrates text alignment txt = [x*"\n"*y for x in ["hleft", "hcenter","hright"], y in ["vtop","vcenter","vbottom"] ] x = repeat(0.1w.*[1,5,9], outer=3) y = repeat(0.1h.*[1,5,9], inner=3) xp = repeat([hleft,hcenter,hright], outer=3) yp = repeat([vtop,vcenter,vbottom], inner=3) img = compose(context(), (context(), circle(x, y, [0.01]), fill("red")), text(x, y, txt, xp, yp), fontsize(14pt) ) ``` ================================================ FILE: docs/src/gallery/properties.md ================================================ ```@meta Author = ["Mattriks"] ``` # [Properties](@id properties_gallery) ## [`arrow`](@ref) ```@example using Compose set_default_graphic_size(14cm,5cm) θ, r = 2π*rand(3), 0.1.+0.08*rand(3) c, s = r.*cos.(θ), r.*sin.(θ) point_array = [[(0.5,0.5), 0.5.+(x,y)] for (x,y) in zip(c,s) ] img = compose(context(), arrow(), stroke("black"), fill(nothing), (context(), arc(0.18, 0.5, 0.08, -π/4, 1π)), (context(), line(point_array), stroke(["red","green","deepskyblue"])), (context(), curve((0.7,0.5), (0.8,-0.5), (0.8,1.5), (0.9,0.5))) ) ``` ## [`clip`](@ref) ```@example using Colors, Compose set_default_graphic_size(14cm,7cm) X = rand(10,2) colv = HSVA.(range(0,stop=180,length=10), 1, 1, 0.5) img = compose(context(units=UnitBox(0,0,2,1)), stroke("black"), (context(), rectangle(), fill(nothing)), (context(), ngon([0.5,1.5],[0.5], [0.4], [10]), fill(nothing), stroke("lightgray")), (context(), xgon(X[:,1],X[:,2],[0.2],[4]), fill(colv)), (context(), xgon(X[:,1].+1,X[:,2],[0.2],[4]), fill(colv), clip(points(ngon(1.5,0.5, 0.4, 10))) ) ) ``` ## [`fill`](@ref), [`fillopacity`](@ref) ```@example using Compose set_default_graphic_size(14cm,4cm) img = compose(context(), (context(), circle(0.5, 0.5, 0.08), fillopacity(0.3), fill("orange")), (context(), circle([0.1, 0.26], [0.5], [0.1]), fillopacity(0.3), fill("blue")), (context(), circle([0.42, 0.58], [0.5], [0.1]), fillopacity(0.3), fill(["yellow","green"])), (context(), circle([0.74, 0.90], [0.5], [0.1]), fillopacity([0.5,0.3]), fill(["yellow","red"]) ) ) ``` ================================================ FILE: docs/src/gallery/transforms.md ================================================ ```@meta Author = ["Mattriks"] ``` # [Transformations](@id transforms_gallery) ## [`Mirror`](@ref) ```@example using Compose set_default_graphic_size(15cm,5cm) f_points = [(.1, .1), (.9, .1), (.9, .2), (.2, .2), (.2, .4), (.6, .4), (.6, .5), (.2, .5), (.2, .9), (.1, .9), (.1, .1)] f_points = (x->0.4.*x.+0.1).(f_points) fpoly(ϕ::Float64) = (context(rotation=Rotation(ϕ,0.3,0.46)), polygon(f_points)) imgfa(θ, ϕ=0.0, x=0.5,y=0.5) = compose(context(), fill("salmon"), fillopacity(1.0), fpoly(ϕ), (context(rotation=Rotation(θ,x,y)), line([(x-0.5,y),(x+0.5,y)]), circle(x,y, 0.02)), (context(mirror=Mirror(θ, x, y)), fpoly(ϕ)) ) Fmir = hstack(imgfa(-π/4), imgfa(-π/2.2), imgfa(π/4, 1π)) img = compose(context(), rectangle(), fill(nothing), stroke("black"), Fmir) ``` ## [`Rotation`](@ref) ```@example using Compose set_default_graphic_size(15cm,5cm) # This example also illustrates nested contexts f_points = [(.1, .1), (.9, .1), (.9, .2), (.2, .2), (.2, .4), (.6, .4), (.6, .5), (.2, .5), (.2, .9), (.1, .9), (.1, .1)] rect(c::String) = (context(), rectangle(), stroke(c)) circ(c::String, s::Float64=0.4) = (context(), circle([x],[y],[0.03,s]), stroke(c)) fpoly(c::String) = (context(), polygon(f_points), fill(c), fillopacity(1.0)) contextC(θ::Float64) = (context(0.5,0.5,1.5,1.5, units=UnitBox(0,0,1,1), rotation=Rotation(θ,x,y)), fpoly("steelblue"), circ("orange")) imgf(θ::Float64) = compose(context(), (context(0.15, 0.15, 0.7, 0.7, units=UnitBox(0,0,2,2)), rect("red"), contextC(θ)) # context C in context B in context A ) x, y, θ = 0.5, 0.25, π/3 Frot = hstack(imgf(-θ), imgf(0.), imgf(θ)) img = compose(context(), rectangle(), fill(nothing), stroke("black"), Frot) ``` ## [`Shear`](@ref) ```@example using Compose set_default_graphic_size(15cm, 5cm) f_points = [(.1, .1), (.9, .1), (.9, .2), (.2, .2), (.2, .4), (.6, .4), (.6, .5), (.2, .5), (.2, .9), (.1, .9), (.1, .1)] f_points = (x->0.5.*x.+0.3).(f_points) ctxl(θ, x, y) = (context(rotation=Rotation(θ, x, y)), circle(x,y, 0.01), line([(x-0.5,y),(x+0.5,y)])) fpoly(c::String) = (context(), polygon(f_points), fill(c) ) ctxf(θ, ϕ, s, x, y,c) = (context(rotation=Rotation(-θ, x, y), shear=Shear(s, ϕ, x, y)), fpoly(c)) x, y, θ = 0.5, 0.5, -π/6 img1 = compose(context(), stroke("black"), ctxl(θ,x,y), ctxf(0,0,0,x,y, "yellow"), (context(), arc(x,y,0.3,π+θ,π-0.15), arrow()) ) img2 = compose(context(), stroke("black"), ctxl(0,x,y), ctxf(θ,0,1.8,x,y,"transparent"), ctxf(θ,0,0,x,y,"yellow"), text(0.5, 0.1, "x' = x+y*shear", hcenter, vcenter) ) img3 = compose(context(), stroke("black"), ctxl(θ, x, y), ctxf(0,θ,1.8,x,y,"yellow") ) hstack(img1, img2, img3) ``` ================================================ FILE: docs/src/index.md ================================================ ```@meta Author = ["Daniel C. Jones", "Gio Borje", "Tamas Nagy"] ``` # Compose Compose is a declarative vector graphics system written in Julia. It's designed to simplify the creation of complex graphics and serves as the basis of the [Gadfly](https://github.com/GiovineItalia/Gadfly.jl) data visualization package. ## Package features - Renders publication quality graphics to SVG, PNG, Postscript, PDF and PGF - Intuitive and consistent interface - Works with [Jupyter](http://jupyter.org/) notebooks via [IJulia](https://github.com/JuliaLang/IJulia.jl) out of the box ## Installation The latest release of **Compose** can be installed from the Julia REPL prompt with ```julia julia> Pkg.add("Compose") ``` This installs the package and any missing dependencies. From there, the simplest of graphics can be rendered to your default internet browser with ```julia julia> using Compose julia> compose(context(), circle(), fill("gold")) ``` Now that you have it installed, check out the [Tutorial](@ref) and the [Forms](@ref forms_gallery) gallery. ## Influences Compose is intended as a futuristic version of the R library [grid](http://www.stat.auckland.ac.nz/~paul/grid/grid.html), and so takes a few ideas from grid. The Compose canvas is roughly equivalent to a viewport in grid, for example. Compose was also inspired by the admirable Haskell library [Diagrams](http://projects.haskell.org/diagrams/). ================================================ FILE: docs/src/library.md ================================================ ```@autodocs Modules = [Compose] ``` ================================================ FILE: docs/src/tutorial.md ================================================ ```@meta Author = ["Daniel C. Jones", "Gio Borje", "Tamas Nagy"] ``` # Tutorial ## Compose is declarative In a declarative graphics system, a figure is built without specifying the precise sequence of drawing commands but by arranging shapes and attaching properties. This makes it easy to break a complex graphic into manageable parts and then figure out how to combine the parts. ## Everything is a tree Graphics in Compose are defined using a tree structure. It's not unlike SVG in this regard, but has simpler semantics. There are three important types that make up the nodes of the tree: * `Context`: An internal node. * `Form`: A leaf node that defines some geometry, like a line or a polygon. * `Property`: A leaf node that modifies how its parent's subtree is drawn, like fill color, font family, or line width. The all-important function in Compose, is called, not surprisingly, `compose`. Calling `compose(a, b)` will return a new tree rooted at `a` and with `b` attached as a child. That's enough to start drawing some simple shapes. ```@setup 1 using Compose set_default_graphic_size(4cm, 4cm) set_default_jsmode(:exclude) ``` ```@example 1 using Compose composition = compose(compose(context(), rectangle()), fill("tomato")) draw(SVG("tomato.svg", 4cm, 4cm), composition) nothing # hide ``` ![](tomato.svg) The last line renders the composition to specificied backend, here the SVG backend. This can also be written like `composition |> SVG("tomato.svg", 4cm, 4cm)`. Alternatively, if multiple compositions of the same size are to be generated, this can be abbreviated even further to ``` set_default_graphic_size(4cm, 4cm) composition |> SVG("tomato.svg") composition2 |> SVG("celery.svg") composition3 |> SVG("rutabaga.svg") # etc... ``` ## The compose function accepts S-expressions In the first example, we had to call `compose` twice just to draw a lousy red square. Fortunately `compose` has a few tricks up its sleeve. As everyone from lisp hackers and [phylogeneticists](http://en.wikipedia.org/wiki/Newick_format) knows, trees can be defined most tersely using S-expressions. We can rewrite our first example like: ```julia # equivalent to compose(compose(context(), rectangle()), fill("tomato"))) compose(context(), rectangle(), fill("tomato")) ``` Furthermore, more complex trees can be formed by grouping subtrees with parenthesis or brackets. ```@example 1 composition = compose(context(), (context(), circle(), fill("bisque")), (context(), rectangle(), fill("tomato"))) composition |> SVG("tomato_bisque.svg") nothing # hide ``` ![](tomato_bisque.svg) ## Trees can be visualized with introspect A useful function for visualizing the graphic that you've constructed is `introspect`. It takes a `Context` defining a graphic and returns a new graphic with a schematic of the tree. ```@example 2 using Compose # hide set_default_graphic_size(6cm, 6cm) # hide tomato_bisque = compose(context(), (context(), circle(), fill("bisque")), (context(), rectangle(), fill("tomato"))) introspect(tomato_bisque) ``` This is a little cryptic, but you can use this limited edition decoder ring: ```@example 2 using Compose, Colors, Measures set_default_graphic_size(6cm, 4cm) figsize = 6mm t = table(3, 2, 1:3, 2:2, y_prop=[1.0, 1.0, 1.0]) t[1,1] = [compose(context(minwidth=figsize + 2mm, minheight=figsize), circle(0.5, 0.5, figsize/2), fill(LCHab(92, 10, 77)))] t[2,1] = [compose(context(minwidth=figsize + 2mm, minheight=figsize), rectangle(0.5cx - figsize/2, 0.5cy - figsize/2, figsize, figsize), fill(LCHab(68, 74, 192)))] t[3,1] = [compose(context(minwidth=figsize + 2mm, minheight=figsize), polygon([(0.5cx - figsize/2, 0.5cy - figsize/2), (0.5cx + figsize/2, 0.5cy - figsize/2), (0.5cx, 0.5cy + figsize/2)]), fill(LCHab(68, 74, 29)))] t[1,2] = [compose(context(), text(0, 0.5, "Context", hleft, vcenter))] t[2,2] = [compose(context(), text(0, 0.5, "Form", hleft, vcenter))] t[3,2] = [compose(context(), text(0, 0.5, "Property", hleft, vcenter))] compose(context(), t, fill(LCHab(92, 10, 77)), fontsize(10pt)) ``` ## Contexts specify a coordinate system for their children In addition to forming internal nodes to group `Form` and `Property` children, a `Context` can define a coordinate system using the `context(x0, y0, width, height)` form. Here we'll reposition some circles by composing them with contexts using different coordinate systems. ```@setup 3 using Compose set_default_graphic_size(4cm, 4cm) ``` ```@example 3 composition = compose(context(), fill("tomato"), (context(0.0, 0.0, 0.5, 0.5), circle()), (context(0.5, 0.5, 0.5, 0.5), circle())) composition |> SVG("tomatos.svg") nothing # hide ``` ![](tomatos.svg) The context's box (i.e. `(x0, y0, width, height)`) is given in terms of its parent's coordinate system and defaults to `(0, 0, 1, 1)`. All the children of a context will use coordinates relative to that box. This is an easy mechanism to translate the coordinates of a subtree in the graphic, but coordinates can be scaled and shifted as well by passing a `UnitBox` to the `units` attribute. ```@example 3 composition = compose(context(), (context(units=UnitBox(0, 0, 1000, 1000)), polygon([(0, 1000), (500, 1000), (500, 0)]), fill("tomato")), (context(), polygon([(1, 1), (0.5, 1), (0.5, 0)]), fill("bisque"))) composition |> SVG("tomato_bisque_triangle.svg") nothing # hide ``` ![](tomato_bisque_triangle.svg) ## Measures can be a combination of absolute and relative units Complex visualizations often are defined using a combination of relative and absolute units. Compose makes these easy. In fact there are four sorts of units used in Compose: * **Context (position) units**: If no unit is explicitly attached to a position, it is assumed to be in “context units”, which are relative to the parent Context's box and coordinate system. (Constants: `cx`, `cy`) * **Context (relative position) units**: The radius of a form can be expressed in size units with respect to the context units (Constants: `sx`, `sy`). If no unit is explicitly attached to a size (e.g. radius) unit, it is assumed to be wrt the context `x` units. More info below. * **Width/Height units**: Sometimes you'll want place geometry in relative coordinates, but bypassing the parent context's coordinate system. Width/height work so that `(0w, 0h)` is always the top-left corner of the context, and `(1w, 1h)` is always the bottom-right. (Constants: `w`, `h`) * **Absolute units**: Absolute units are inches, centimeters, points, etc. (Constants: `inch`, `cm`, `mm`, `pt`) Any linear combination of these types of units is allowed. For example, `1w - 10mm` is a well formed expression, giving the width of the parent canvas minus ten millimeters. An example of the difference between context position units (`cx`, `cy`) and size units (`sx`, `sy`), is when the coordinate system (defined by `UnitBox`) does not start at `(0,0)`, then the size `0sx` (or `0sy`) will always equal `0mm`, but the position `(0cx, 0cy)` will be dependent on the coordinates (and may refer to a point outside the context's `UnitBox`). ## Forms and Properties can be vectorized Often one needs to produce many copies of a similar shape. Most of the forms an properties have a scalar and vector forms to simplify this sort of mass production. We'll use `circle` as an example, which has two constructors: ```julia circle(x=0.5w, y=0.5h, r=0.5w) circle(xs::AbstractArray, ys::AbstractArray, rs::AbstractArray) ``` The first of these creates only circle centered at `(x, y)` with radius `r`. The second form can succinctly create many circles (using the [Colors](https://github.com/JuliaLang/Colors.jl) package to specify the `LHCab` colorspace): ```@setup 4 using Compose, Colors set_default_graphic_size(4cm, 4cm) ``` ```@example 4 composition = compose(context(), circle([0.25, 0.5, 0.75], [0.25, 0.5, 0.75], [0.1, 0.1, 0.1]), fill(LCHab(92, 10, 77))) composition |> SVG("circles.svg") nothing # hide ``` ![](circles.svg) The arrays in passed to `xs`, `ys`, and `rs` need not be the same length. Shorter arrays will be cycled. This let's us shorten this last example by only specifying the radius just once. ```@example 4 composition = compose(context(), circle([0.25, 0.5, 0.75], [0.25, 0.5, 0.75], [0.1]), fill(LCHab(92, 10, 77))) composition |> SVG("cycled_circles.svg") nothing # hide ``` ![](cycled_circles.svg) The `fill` is a property can also be vectorized here to quickly assign different colors to each circle. ```@example 4 circles_fill_vectorized = compose(context(), circle([0.25, 0.5, 0.75], [0.25, 0.5, 0.75], [0.1]), fill([LCHab(92, 10, 77), LCHab(68, 74, 192), LCHab(78, 84, 29)])) circles_fill_vectorized |> SVG("circles_fill_vectorized.svg") nothing # hide ``` ![](circles_fill_vectorized.svg) If vector properties are used with vector forms, they must be of equal length. ## Compose can produce arbitrary directed graphs Though we've so far explained `compose` as producing trees, there's nothing stopping one from producing an arbitrary directed graph. This can be quite useful in some cases. In this example, only one triangle object is ever initialized, despite many triangles being drawn, which is possible because the graph produced by `siepinski` is not a tree. The triangle polygon has many parent nodes than “re-contextualize” that triangle by repositioning it. ```@example 5 using Compose, Colors # hide set_default_graphic_size(8cm, 8*(sqrt(3)/2)*cm) # hide function sierpinski(n) if n == 0 compose(context(), polygon([(1,1), (0,1), (1/2, 0)])) else t = sierpinski(n - 1) compose(context(), (context(1/4, 0, 1/2, 1/2), t), (context( 0, 1/2, 1/2, 1/2), t), (context(1/2, 1/2, 1/2, 1/2), t)) end end composition = compose(sierpinski(6), fill(LCHab(92, 10, 77))) composition |> SVG("sierpinski.svg", 8cm, 8*(sqrt(3)/2)*cm) nothing # hide ``` ![](sierpinski.svg) There are no safeguards to check for cycles. You can produce a graph with a cycle and Compose will run in an infinite loop trying to draw it. In most applications, this isn't a concern. ## Fancier compositions There are fancier forms of the `compose` function, in particular, variadic `compose`, which is roughly defined as: ```julia compose(a, b, cs...) = compose(compose(a, b), cs...) ``` Compose over tuples or arrays: ```julia compose((as...)) = compose(as...) ``` In effect, this lets one write a complex series of compose operations as an S-expression. For example: ```julia compose(a, b, ((c, d), (e, f), g)) ``` Since all we are doing is building trees, this syntax tends to be pretty convenient. ## [Forms](@ref forms_gallery) These are basic constructors for the in-built forms - see the [Forms gallery](@ref forms_gallery) for examples. * `polygon(points)` * `rectangle(x0, y0, width, height)` * `circle(x, y, r)` * `ellipse(x, y, x_radius, y_radius)` * `text(x, y, value)` * `line(points)` * `curve(anchor0, ctrl0, ctrl1, anchor1)` * `bitmap(mime, data, x0, y0, width, height)` * `arc(x, y, r, angle1, angle2, sector)` * `sector(x, y, r, angle1, angle2)` * `bezigon(anchor0, sides)` ## [Properties](@ref properties_gallery) Properties include `arrow`, `fill`, `fillopacity`, etc. See the [Properties gallery](@ref properties_gallery) for examples. For colors, Compose supports the colors available in [Colors.jl](https://github.com/JuliaGraphics/Colors.jl), which includes many [color spaces](http://juliagraphics.github.io/Colors.jl/stable/), hex strings, and [named colors](http://juliagraphics.github.io/Colors.jl/stable/namedcolors/). ## Text Symbols can be used in text strings by inserting [HTML codes](http://www.ascii.cl/htmlcodes.htm). More general formatting for the SVG backend is [documented here](https://www.w3.org/TR/SVG/text.html), whereas the Cairo backend uses a [Pango markup language](https://developer.gnome.org/pango/unstable/PangoMarkupFormat.html). ```@example 6 using Compose # hide cents_ina_dollar = compose(context(), text(0.5, 0.5,"100¢ in a $")) cents_ina_dollar |> SVG("dollar.svg",5cm,1cm) nothing # hide ``` ![](dollar.svg) Use the `font` and `fontsize` properties to change the appearance of type: ```@example 7 using Compose # hide compose(context(), (context(), text(0.2,0.5,"big"), fontsize(18pt)), (context(), text(0.4,0.5,"small"), fontsize(6pt)), (context(), text(0.6,0.5,"bold"), font("Helvetica-Bold")), (context(), text(0.8,0.5,"oblique"), font("Helvetica-Oblique"))) |> SVG("font_fontsize.svg",15cm,1cm) nothing # hide ``` ![](font_fontsize.svg) ================================================ FILE: src/Compose.jl ================================================ module Compose using Colors using IterTools using DataStructures using Measures using Requires using Dates using Printf using Base.Iterators using Statistics import JSON import Base: length, isempty, getindex, setindex!, display, show, convert, zero, isless, max, fill, size, copy, min, max, abs, +, -, *, /, == import Measures: resolve, w, h export compose, compose!, Context, UnitBox, AbsoluteBoundingBox, Rotation, Mirror, Shear, ParentDrawContext, context, ctxpromise, table, set_units!, minwidth, minheight, text_extents, max_text_extents, polygon, ngon, star, xgon, bezigon, line, rectangle, circle, arc, sector, ellipse, text, curve, bitmap, stroke, fill, strokedash, strokelinecap, arrow, strokelinejoin, linewidth, visible, fillopacity, strokeopacity, clip, points, font, fontsize, svgid, svgclass, svgattribute, jsinclude, jscall, Measure, inch, mm, cm, pt, px, cx, cy, sx, sy, w, h, hleft, hcenter, hright, vtop, vcenter, vbottom, SVG, SVGJS, PGF, PNG, PS, PDF, draw, pad, pad_inner, pad_outer, hstack, vstack, gridstack, LineCapButt, LineCapSquare, LineCapRound, CAIROSURFACE, introspect, set_default_graphic_size, set_default_jsmode, boundingbox, Patchable abstract type Backend end """ Some backends can more efficiently draw forms by batching. If so, they shuld define a similar method that returns true. """ canbatch(::Backend) = false # Allow users to supply strings without deprecation warnings parse_colorant(c::Colorant) = c parse_colorant(str::AbstractString) = parse(Colorant, str) parse_colorant(c::Union{Tuple,Vector}) = [parse_colorant(x) for x in c] parse_colorant(c...) = parse_colorant(c) @deprecate parse_colorant_vec(c...) parse_colorant(c) include("misc.jl") include("measure.jl") include("list.jl") # Every graphic in Compose consists of a tree. abstract type ComposeNode end # Used to mark null child pointers struct NullNode <: ComposeNode end nullnode = NullNode() include("form.jl") include("property.jl") include("container.jl") include("batch.jl") include("table.jl") include("stack.jl") # How large to draw graphics when not explicitly drawing to a backend default_graphic_width = sqrt(2)*10*cm default_graphic_height = 10cm function set_default_graphic_size(width::MeasureOrNumber, height::MeasureOrNumber) global default_graphic_width global default_graphic_height default_graphic_width = x_measure(width) default_graphic_height = y_measure(height) nothing end default_graphic_format = :html function set_default_graphic_format(fmt::Symbol) fmt in [:html, :png, :svg, :pdf, :ps, :pgf] || error("$(fmt) is not a supported plot format") global default_graphic_format default_graphic_format = fmt nothing end # Default means to include javascript dependencies in the SVGJS backend. default_jsmode = :embed function set_default_jsmode(mode::Symbol) global default_jsmode if mode in [:none, :exclude, :embed, :linkabs, :linkrel] default_jsmode = mode else error("$(mode) is not a valid jsmode") end nothing end function default_mime() if default_graphic_format == :png "image/png" elseif default_graphic_format == :svg "image/svg+xml" elseif default_graphic_format == :html "text/html" elseif default_graphic_format == :ps "application/postscript" elseif default_graphic_format == :pdf "application/pdf" elseif default_graphic_format == :pgf "application/x-tex" else "" end end # Default property values default_font_family = "Helvetica Neue,Helvetica,Arial,sans" default_font_size = 11pt default_line_width = 0.3mm default_stroke_color = nothing default_fill_color = colorant"black" # If Cairo is not available, throw an error when trying to save with a Cairo backend missing_cairo_error(backend::String, invocation::String=backend) = """ The Cairo and Fontconfig packages are necessary for saving as $backend. Add them with the package manager if necessary, then run `import Cairo, Fontconfig` before invoking `$invocation`. """ missing_cairo_error(m::MIME) = missing_cairo_error(string(m), "show(::IO, ::MIME\"$m\", ::Context)") for backend in [:PNG, :PS, :PDF] docstr = missing_cairo_error(string(backend)) @eval @doc $docstr $backend(args...; kwargs...) = error(missing_cairo_error(string($backend))) end CairoMIME = Union{MIME"image/png", MIME"application/ps", MIME"application/pdf"} show(io::IO, m::CairoMIME, ctx::Context) = error(missing_cairo_error(m)) include("svg.jl") include("pgf_backend.jl") # If available, pango and fontconfig are used to compute text extents and match # fonts. Otherwise a simplistic pure-julia fallback is used. include("fontfallback.jl") const pango_cairo_ctx = Ref{Ptr}(C_NULL) const pango_cairo_fm = Ref{Ptr}(C_NULL) const pangolayout = Ref{Any}(nothing) function link_fontconfig() @debug "Loading Fontconfig backend into Compose.jl" pango_cairo_ctx[] = C_NULL ccall((:g_type_init, libgobject), Cvoid, ()) pango_cairo_fm[] = ccall((:pango_cairo_font_map_new, libpangocairo), Ptr{Cvoid}, ()) pango_cairo_ctx[] = ccall((:pango_font_map_create_context, libpango), Ptr{Cvoid}, (Ptr{Cvoid},), pango_cairo_fm[]) pangolayout[] = PangoLayout() end function link_cairo() @debug "Loading Cairo backend into Compose.jl" include(joinpath(@__DIR__,"cairo_backends.jl")) include(joinpath(@__DIR__,"immerse_backend.jl")) end function __init__() @require Cairo="159f3aea-2a34-519c-b102-8c37f9878175" link_cairo() @require Fontconfig="186bb1d3-e1f7-5a2c-a377-96d770f13627" begin include("pango.jl") link_fontconfig() end end show(io::IO, m::MIME"text/html", ctx::Context) = draw(SVGJS(io, default_graphic_width, default_graphic_height, false, jsmode=default_jsmode), ctx) show(io::IO, m::MIME"image/svg+xml", ctx::Context) = draw(SVG(io, default_graphic_width, default_graphic_height, false), ctx) function pad_outer(c::Context, left_padding::MeasureOrNumber, right_padding::MeasureOrNumber, top_padding::MeasureOrNumber, bottom_padding::MeasureOrNumber) left_padding = size_measure(left_padding) right_padding = size_measure(right_padding) top_padding = size_measure(top_padding) bottom_padding = size_measure(bottom_padding) root = context(c.box.x0[1], c.box.x0[1], c.box.a[1] + left_padding + right_padding, c.box.a[2] + top_padding + bottom_padding, minwidth=c.minwidth, minheight=c.minheight) c = copy(c) c.box = BoundingBox(left_padding, top_padding, 1w - left_padding - right_padding, 1h - top_padding - bottom_padding) return compose!(root, c) end pad_outer(c::Context, padding::MeasureOrNumber) = pad_outer(c, padding, padding, padding, padding) pad_outer(cs::Vector{Context}, left_padding::MeasureOrNumber, right_padding::MeasureOrNumber, top_padding::MeasureOrNumber, bottom_padding::MeasureOrNumber) = map(c -> pad_outer(c, left_padding, right_padding, top_padding, bottom_padding), cs) pad_outer(cs::Vector{Context}, padding::MeasureOrNumber) = pad_outer(cs, padding, padding, padding, padding) function pad_inner(c::Context, left_padding::MeasureOrNumber, right_padding::MeasureOrNumber, top_padding::MeasureOrNumber, bottom_padding::MeasureOrNumber) left_padding = size_measure(left_padding) right_padding = size_measure(right_padding) top_padding = size_measure(top_padding) bottom_padding = size_measure(bottom_padding) root = context(c.box.x0[1], c.box.x0[2], c.box.a[1], c.box.a[2], minwidth=c.minwidth, minheight=c.minheight) c = copy(c) c.box = BoundingBox(left_padding, top_padding, 1w - left_padding - right_padding, 1h - top_padding - bottom_padding) return compose!(root, c) end pad_inner(c::Context, padding::MeasureOrNumber) = pad_inner(c, padding, padding, padding, padding) pad_inner(cs::Vector{Context}, left_padding::MeasureOrNumber, right_padding::MeasureOrNumber, top_padding::MeasureOrNumber, bottom_padding::MeasureOrNumber) = map(c -> pad_inner(c, left_padding, right_padding, top_padding, bottom_padding), cs) pad_inner(cs::Vector{Context}, padding::MeasureOrNumber) = pad_inner(cs, padding, padding, padding, padding) function gridstack(cs::Matrix{Context}) m, n = size(cs) t = Table(m, n, 1:m, 1:n, x_prop=ones(n), y_prop=ones(m)) for i in 1:m, j in 1:n t[i, j] = [cs[i, j]] end return compose!(context(), t) end const pad = pad_outer end # module Compose ================================================ FILE: src/abandoned.jl ================================================ # Path Primitives # From form.jl: # Path # ---- # An implementation of the SVG path mini-language. abstract type PathOp end struct MoveAbsPathOp <: PathOp to::Vec end function assert_pathop_tokens_len(op_type, tokens, i, needed) provided = length(tokens) - i + 1 provided < needed && error("In path $(op_type) requires $(needed) argumens but only $(provided) provided.") end function parsepathop(::Type{MoveAbsPathOp}, tokens::AbstractArray, i) assert_pathop_tokens_len(MoveAbsPathOp, tokens, i, 2) op = MoveAbsPathOp((x_measure(tokens[i]), y_measure(tokens[i + 1]))) return (op, i + 2) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::MoveAbsPathOp) = MoveAbsPathOp(resolve(box, units, t, p.to)) struct MoveRelPathOp <: PathOp to::Vec end function parsepathop(::Type{MoveRelPathOp}, tokens::AbstractArray, i) assert_pathop_tokens_len(MoveRelPathOp, tokens, i, 2) op = MoveRelPathOp((tokens[i], tokens[i + 1])) return (op, i + 2) end function resolve_offset(box::AbsoluteBox, units::UnitBox, t::Transform, p::Vec) absp = resolve(box, units, t, p) zer0 = resolve(box, units, t, (0w, 0h)) return (absp[1] - zer0[1], absp[2] - zer0[2]) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::MoveRelPathOp) = MoveRelPathOp(resolve_offset(box, units, t, p.to)) struct ClosePathOp <: PathOp end parsepathop(::Type{ClosePathOp}, tokens::AbstractArray, i) = (ClosePathOp(), i) resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::ClosePathOp) = p struct LineAbsPathOp <: PathOp to::Vec end function parsepathop(::Type{LineAbsPathOp}, tokens::AbstractArray, i) assert_pathop_tokens_len(LineAbsPathOp, tokens, i, 2) op = LineAbsPathOp((x_measure(tokens[i]), y_measure(tokens[i + 1]))) return (op, i + 2) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::LineAbsPathOp) = LineAbsPathOp(resolve(box, units, t, p.to)) struct LineRelPathOp <: PathOp to::Vec end function parsepathop(::Type{LineRelPathOp}, tokens::AbstractArray, i) assert_pathop_tokens_len(LineRelPathOp, tokens, i, 2) op = LineRelPathOp((x_measure(tokens[i]), y_measure(tokens[i + 1]))) return (op, i + 2) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::LineRelPathOp) = LineRelPathOp(resolve(box, units, t, p.to)) struct HorLineAbsPathOp <: PathOp x::Measure end function parsepathop(::Type{HorLineAbsPathOp}, tokens::AbstractArray, i) assert_pathop_tokens_len(HorLineAbsPathOp, tokens, i, 1) op = HorLineAbsPathOp(x_measure(tokens[i])) return (op, i + 1) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::HorLineAbsPathOp) = HorLineAbsPathOp(resolve(box, units, t, (p.x, 0mm))[1]) struct HorLineRelPathOp <: PathOp Δx::Measure end function parsepathop(::Type{HorLineRelPathOp}, tokens::AbstractArray, i) assert_pathop_tokens_len(HorLineRelPathOp, tokens, i, 1) op = HorLineRelPathOp(x_measure(tokens[i])) return (op, i + 1) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::HorLineRelPathOp) = HorLineRelPathOp(resolve(box, units, t, p.Δx)) struct VertLineAbsPathOp <: PathOp y::Measure end function parsepathop(::Type{VertLineAbsPathOp}, tokens::AbstractArray, i) assert_pathop_tokens_len(VertLineAbsPathOp, tokens, i, 1) op = VertLineAbsPathOp(y_measure(tokens[i])) return (op, i + 1) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::VertLineAbsPathOp) = VertLineAbsPathOp(resolve(box, units, t, (0mm, p.y))[2]) struct VertLineRelPathOp <: PathOp Δy::Measure end function parsepathop(::Type{VertLineRelPathOp}, tokens::AbstractArray, i) assert_pathop_tokens_len(VertLineRelPathOp, tokens, i, 1) op = VertLineRelPathOp(y_measure(tokens[i])) return (op, i + 1) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::VertLineRelPathOp) = VertLineAbsPathOp(resolve(box, units, t, (0mmm, p.Δy))[2]) struct CubicCurveAbsPathOp <: PathOp ctrl1::Vec ctrl2::Vec to::Vec end function parsepathop(::Type{CubicCurveAbsPathOp}, tokens::AbstractArray, i) assert_pathop_tokens_len(CubicCurveAbsPathOp, tokens, i, 6) op = CubicCurveAbsPathOp((tokens[i], tokens[i + 1]), (tokens[i + 2], tokens[i + 3]), (tokens[i + 4], tokens[i + 5])) return (op, i + 6) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::CubicCurveAbsPathOp) = CubicCurveAbsPathOp( resolve(box, units, t, p.ctrl1), resolve(box, units, t, p.ctrl2), resolve(box, units, t, p.to)) struct CubicCurveRelPathOp <: PathOp ctrl1::Vec ctrl2::Vec to::Vec end function parsepathop(::Type{CubicCurveRelPathOp}, tokens::AbstractArray, i) assert_pathop_tokens_len(CubicCurveRelPathOp, tokens, i, 6) op = CubicCurveRelPathOp((tokens[i], tokens[i + 1]), (tokens[i + 2], tokens[i + 3]), (tokens[i + 4], tokens[i + 5])) return (op, i + 6) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::CubicCurveRelPathOp) = CubicCurveRelPathOp( resolve(box, units, t, p.ctrl1), resolve(box, units, t, p.ctrl2), resolve(box, units, t, p.to)) struct CubicCurveShortAbsPathOp <: PathOp ctrl2::Vec to::Vec end function parsepathop(::Type{CubicCurveShortAbsPathOp}, tokens::AbstractArray, i) assert_pathop_tokens_len(CubicCurveShortAbsPathOp, tokens, i, 4) op = CubicCurveShortAbsPathOp((x_measure(tokens[i]), y_measure(tokens[i + 1])), (x_measure(tokens[i + 2]), y_measure(tokens[i + 3]))) return (op, i + 4) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::CubicCurveShortAbsPathOp) = CubicCurveShortAbsPathOp( resolve_offset(box, units, t, p.ctrl2), resolve_offset(box, units, t, p.to)) struct CubicCurveShortRelPathOp <: PathOp ctrl2::Vec to::Vec end function parsepathop(::Type{CubicCurveShortRelPathOp}, tokens::AbstractArray, i) assert_pathop_tokens_len(CubicCurveShortRelPathOp, tokens, i, 4) op = CubicCurveShortRelPathOp((x_measure(tokens[i]), y_measure(tokens[i + 1])), (x_measure(tokens[i + 2]), y_measure(tokens[i + 3]))) return (op, i + 4) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::CubicCurveShortRelPathOp) = CubicCurveShortRelPathOp( resolve(box, units, t, p.ctrl2), resolve(box, units, t, p.to)) struct QuadCurveAbsPathOp <: PathOp ctrl1::Vec to::Vec end function parsepathop(::Type{QuadCurveAbsPathOp}, tokens::AbstractArray, i) assert_pathop_tokens_len(QuadCurveAbsPathOp, tokens, i, 4) op = QuadCurveAbsPathOp((tokens[i], tokens[i + 1]), (tokens[i + 2], tokens[i + 3])) return (op, i + 4) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::QuadCurveAbsPathOp) = QuadCurveAbsPathOp( resolve(box, units, t, p.ctrl1), resolve(box, units, t, p.to)) struct QuadCurveRelPathOp <: PathOp ctrl1::Vec to::Vec end function parsepathop(::Type{QuadCurveRelPathOp}, tokens::AbstractArray, i) assert_pathop_tokens_len(QuadCurveRelPathOp, tokens, i, 4) op = QuadCurveRelPathOp((tokens[i], tokens[i + 1]), (tokens[i + 2], tokens[i + 3])) return (op, i + 4) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::QuadCurveRelPathOp) = QuadCurveRelPathOp( (resolve(box, units, t, p.ctrl1[1]), resolve(box, units, t, p.ctrl1[2])), (resolve(box, units, t, p.to[1]), resolve(box, units, t, p.to[2]))) struct QuadCurveShortAbsPathOp <: PathOp to::Vec end function parsepathop(::Type{QuadCurveShortAbsPathOp}, tokens::AbstractArray, i) assert_pathop_tokens_len(QuadCurveShortAbsPathOp, tokens, i, 2) op = QuadCurveShortAbsPathOp((tokens[i], tokens[i + 1])) return (op, i + 2) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::QuadCurveShortAbsPathOp) = QuadCurveShortAbsPathOp(resolve(box, units, t, p.to)) struct QuadCurveShortRelPathOp <: PathOp to::Vec end function parsepathop(::Type{QuadCurveShortRelPathOp}, tokens::AbstractArray, i) assert_pathop_tokens_len(QuadCurveShortRelPathOp, tokens, i, 2) op = QuadCurveShortRelPathOp((tokens[i], tokens[i + 1])) return (op, i + 2) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::QuadCurveShortRelPathOp) = QuadCurveShortRelPathOp( (resolve(box, units, t, p.to[1]), resolve(box, units, t, p.to[2]))) struct ArcAbsPathOp <: PathOp rx::Measure ry::Measure rotation::Float64 largearc::Bool sweep::Bool to::Vec end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::ArcAbsPathOp) = ArcAbsPathOp( resolve(box, units, t, p.rx), resolve(box, units, t, p.ry), p.rotation, p.largearc, p.sweep, resolve(box, units, t, p.to)) struct ArcRelPathOp <: PathOp rx::Measure ry::Measure rotation::Float64 largearc::Bool sweep::Bool to::Vec end function parsepathop(::Type{T}, tokens::AbstractArray, i) where T <: Union{ArcAbsPathOp, ArcRelPathOp} assert_pathop_tokens_len(T, tokens, i, 7) if isa(tokens[i + 3], Bool) largearc = tokens[i + 3] elseif tokens[i + 3] == 0 largearc = false elseif tokens[i + 3] == 1 largearc = true else error("largearc argument to the arc path operation must be boolean") end if isa(tokens[i + 4], Bool) sweep = tokens[i + 4] elseif tokens[i + 4] == 0 sweep = false elseif tokens[i + 4] == 1 sweep = true else error("sweep argument to the arc path operation must be boolean") end isa(tokens[i + 2], Number) || error("path arc operation requires a numerical rotation") op = T(x_measure(tokens[i]), y_measure(tokens[i + 1]), convert(Float64, tokens[i + 2]), largearc, sweep, (x_measure(tokens[i + 5]), y_measure(tokens[i + 6]))) return (op, i + 7) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::ArcRelPathOp) = ArcRelPathOp( resolve(box, units, t, p.rx), resolve(box, units, t, p.ry), p.rotation, p.largearc, p.sweep, (resolve(box, units, t, p.to[1]), resolve(box, units, t, p.to[2]))) const path_ops = Dict( :M => MoveAbsPathOp, :m => MoveRelPathOp, :Z => ClosePathOp, :z => ClosePathOp, :L => LineAbsPathOp, :l => LineRelPathOp, :H => HorLineAbsPathOp, :h => HorLineRelPathOp, :V => VertLineAbsPathOp, :v => VertLineRelPathOp, :C => CubicCurveAbsPathOp, :c => CubicCurveRelPathOp, :S => CubicCurveShortAbsPathOp, :s => CubicCurveShortRelPathOp, :Q => QuadCurveAbsPathOp, :q => QuadCurveRelPathOp, :T => QuadCurveShortAbsPathOp, :t => QuadCurveShortRelPathOp, :A => ArcAbsPathOp, :a => ArcRelPathOp ) # A path is an array of symbols, numbers, and measures following SVGs path # mini-language. function parsepath(tokens::AbstractArray) ops = PathOp[] last_op_type = nothing i = 1 while i <= length(tokens) tok = tokens[i] strt = i if isa(tok, Symbol) if !haskey(path_ops, tok) error("$(tok) is not a valid path operation") else op_type = path_ops[tok] i += 1 op, i = parsepathop(op_type, tokens, i) push!(ops, op) last_op_type = op_type end else op, i = parsepathop(last_op_type, tokens, i) push!(ops, op) end end return ops end struct PathPrimitive <: FormPrimitive ops::Vector{PathOp} end const Path = Form{PathPrimitive} path(tokens::AbstractArray, tag=empty_tag) = Path([PathPrimitive(parsepath(tokens))], tag) path(tokens::AbstractArray{T}, tag=empty_tag) where T <: AbstractArray = Path([PathPrimitive(parsepath(ts)) for ts in tokens], tag) resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::PathPrimitive) = PathPrimitive([resolve(box, units, t, op) for op in p.ops]) # TODO: boundingbox # From cairo_backends.jl: function draw(img::Image, prim::PathPrimitive) for op in prim.ops draw_path_op(img, op) end fillstroke(img) end draw_path_op(img::Image, op::MoveAbsPathOp) = move_to(img, op.to) draw_path_op(img::Image, op::MoveRelPathOp) = rel_move_to(img, op.to) draw_path_op(img::Image, op::ClosePathOp) = close_path(img) draw_path_op(img::Image, op::LineAbsPathOp) = line_to(img, op.to) draw_path_op(img::Image, op::LineRelPathOp) = rel_line_to(img, op.to) function draw_path_op(img::Image, op::HorLineAbsPathOp) pos = current_point(img) line_to(img, (op.x, pos.y)) end draw_path_op(img::Image, op::HorLineRelPathOp) = rel_line_to(img, (op.Δx, 0.0mm)) function draw_path_op(img::Image, op::VertLineAbsPathOp) pos = current_point(img) line_to(img, (pos.x, op.y)) end draw_path_op(img::Image, op::VertLineRelPathOp) = rel_line_to(img, (0.0mm, op.Δy)) function draw_path_op(img::Image, op::CubicCurveAbsPathOp) curve_to(img, op.ctrl1, op.ctrl2, op.to) img.last_ctrl2_point = op.ctrl2 end function draw_path_op(img::Image, op::CubicCurveRelPathOp) xy = current_point(img) rel_curve_to(img, op.ctrl1, op.ctrl2, op.to) img.last_ctrl2_point = (op.ctrl2[1] + xy[1], op.ctrl2[2] + xy[2]) end function draw_path_op(img::Image, op::CubicCurveShortAbsPathOp) xy = current_point(img) ctrl1 = img.last_ctrl2_point if ctrl1 === nothing ctrl1 = xy else ctrl1 = (2*xy[1] - ctrl1[1], 2*xy[2] - ctrl1[2]) end curve_to(img, ctrl1, op.ctrl2, op.to) img.last_ctrl2_point = op.ctrl2 end function draw_path_op(img::Image, op::CubicCurveShortRelPathOp) xy = current_point(img) x1, y1 = xy[1].value, xy[2].value x2, y2 = op.to[1].value, op.to[2].value ctrl1 = img.last_ctrl2_point if ctrl1 === nothing ctrl1 = xy else ctrl1 = (Measure(abs=(2*x1 - ctrl1[1].value) - x1), Measure(abs=(2*y1 - ctrl1[2].value) - y1)) end cx, cy = ctrl1[1].value, ctrl1[2].value rel_curve_to(img, ctrl1, op.ctrl2, op.to) img.last_ctrl2_point = (Measure(abs=op.ctrl2[1].value + xy.x.abs), Measure(abs=op.ctrl2[2].value + xy.y.abs)) end function draw_path_op(img::Image, op::QuadCurveAbsPathOp) xy = current_point(img) x1, y1 = xy[1].value, xy[2].value x2, y2 = op.to[1].value, op.to[2].value cx, cy = op.ctrl1[1].value, op.ctrl1[2].value curve_to(img, (Measure(abs=(x1 + 2*cx)/3), Measure(abs=(y1 + 2*cy)/3)), (Measure(abs=(x2 + 2*cx)/3), Measure(abs=(y2 + 2*cy)/3)), op.to) img.last_ctrl1_point = op.ctrl1 end function draw_path_op(img::Image, op::QuadCurveRelPathOp) xy = current_point(img) x1, y1 = xy[1].value, xy[2].value x2, y2 = op.to[1].value, op.to[2].value cx, cy = op.ctrl1[1].value, op.ctrl1[2].value rel_curve_to(img, (Measure(abs=(x1 + 2*cx)/3), Measure(abs=(y1 + 2*cy)/3)), (Measure(abs=(x2 + 2*cx)/3), Measure(abs=(y2 + 2*cy)/3)), op.to) img.last_ctrl1_point = (Measure(abs=op.ctrl1[1].value + xy.x.abs), Measure(abs=op.ctrl1[2].value + xy.y.abs)) end function draw_path_op(img::Image, op::QuadCurveShortAbsPathOp) xy = current_point(img) x1, y1 = xy[1].value, xy[2].value x2, y2 = op.to[1].value, op.to[2].value ctrl1 = img.last_ctrl1_point if img.last_ctrl1_point === nothing ctrl1 = xy else ctrl1 = (Measure(abs=2*x1 - ctrl1[1].value), Measure(abs=2*y1 - ctrl1[2].value)) end cx, cy = ctrl1[1].value, ctrl1[2].value curve_to(img, (Measure(abs=(x1 + 2*cx)/3), Measure(abs=(y1 + 2*cy)/3)), (Measure(abs=(x2 + 2*cx)/3), Measure(abs=(y2 + 2*cy)/3)), (Measure(abs=x2), Measure(abs=y2))) img.last_ctrl1_point = ctrl1 end function draw_path_op(img::Image, op::QuadCurveShortRelPathOp) xy = current_point(img) x1, y1 = xy[1].value, xy[2].value x2, y2 = x1 + op.to[1].value, y1 + op.to[2].value ctrl1 = img.last_ctrl1_point if ctrl1 === nothing ctrl1 = xy else ctrl1 = (Measure(abs=(2*x1 - ctrl1[1].value) - x1), Measure(abs=(2*y1 - ctrl1[2].value) - y1)) end cx, cy = ctrl1[1].value, ctrl1[2].value rel_curve_to(img, (Measure(abs=(x1 + 2*cx)/3), Measure(abs=(y1 + 2*cy)/3)), (Measure(abs=(x2 + 2*cx)/3), Measure(abs=(y2 + 2*cy)/3)), (Measure(abs=x2), Measure(abs=y2))) img.last_ctrl1_point = (Measure(abs=op.ctrl1[1].value + x1), Measure(abs=op.ctrl1[2].value + y1)) end function draw_path_op(img::Image, op::ArcAbsPathOp) xy = current_point(img) x1, y1 = xy[1].value, xy[2].value x2, y2 = op.to[1].value, op.to[2].value rx, ry = op.rx.abs, op.ry.abs φ = deg2rad(op.rotation) draw_endpoint_arc(img, rx, ry, φ, op.largearc, op.sweep, x1, y1, x2, y2) end function draw_path_op(img::Image, op::ArcRelPathOp) xy = current_point(img) x1, y1 = xy[1].value, xy[2].value x2, y2 = x1 + op.to[1].value, y1 + op.to[2].value rx, ry = op.rx.abs, op.ry.abs φ = deg2rad(op.rotation) draw_endpoint_arc(img, rx, ry, φ, op.largearc, op.sweep, x1, y1, x2, y2) end # Draw an SVG style elliptical arc function draw_endpoint_arc(img::Image, rx::Float64, ry::Float64, φ::Float64, largearc::Bool, sweep::Bool, x1::Float64, y1::Float64, x2::Float64, y2::Float64) function uvangle(ux, uy, vx, vy) t = (ux * vx + uy * vy) / (sqrt(ux^2 + uy^2) * sqrt(vx^2 + vy^2)) t = max(min(t, 1.0), -1.0) return (ux * vy - uy * vx < 0.0 ? -1 : 1.0) * acos(t) end # From: http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter xm, ym = (x1 - x2)/2, (y1 - y2)/2 x1p = cos(φ) * xm + sin(φ) * ym y1p = -sin(φ) * xm + cos(φ) * ym u = (rx^2 * ry^2 - rx^2 * y1p^2 - ry^2 * x1p^2) / (rx^2 * y1p^2 + ry^2 * x1p^2) u = u >= 0.0 ? sqrt(u) : 0.0 cxp = u * (rx * y1p) / ry cyp = -u * (ry * x1p) / rx if sweep == largearc cxp = -cxp cyp = -cyp end cx = (x1 + x2)/2 + cos(φ) * cxp - sin(φ) * cyp cy = (y1 + y2)/2 + sin(φ) * cxp + cos(φ) * cyp θ1 = uvangle(1.0, 0.0, (x1p - cxp) / rx, (y1p - cyp) / ry) Δθ = uvangle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry) % (2.0*π) if Δθ > 0.0 && !sweep Δθ -= 2*π elseif Δθ < 0.0 && sweep Δθ += 2*π end Cairo.save(img.ctx) Cairo.translate(img.ctx, absolute_native_units(img, cx), absolute_native_units(img, cy)) Cairo.rotate(img.ctx, φ) Cairo.scale(img.ctx, rx, ry) if sweep arc(img, 0.0, 0.0, 1.0, θ1, θ1 + Δθ) else arc_negative(img, 0.0, 0.0, 1.0, θ1, θ1 + Δθ) end Cairo.restore(img.ctx) end # From svg.jl: function svg_print_path_op(io::IO, op::MoveAbsPathOp) print(io, 'M') svg_print_float(io, op.to[1].value) print(io, ' ') svg_print_float(io, op.to[2].value) end function svg_print_path_op(io::IO, op::MoveRelPathOp) print(io, 'm') svg_print_float(io, op.to[1].value) print(io, ' ') svg_print_float(io, op.to[2].value) end svg_print_path_op(io::IO, op::ClosePathOp) = print(io, 'z') function svg_print_path_op(io::IO, op::LineAbsPathOp) print(io, 'L') svg_print_float(io, op.to[1].value) print(io, ' ') svg_print_float(io, op.to[2].value) end function svg_print_path_op(io::IO, op::LineRelPathOp) print(io, 'l') svg_print_float(io, op.to[1].value) print(io, ' ') svg_print_float(io, op.to[2].value) end function svg_print_path_op(io::IO, op::HorLineAbsPathOp) print(io, 'H') svg_print_float(io, op.x.value) end function svg_print_path_op(io::IO, op::HorLineRelPathOp) print(io, 'h') svg_print_float(io, op.Δx.value) end function svg_print_path_op(io::IO, op::VertLineAbsPathOp) print(io, 'V') svg_print_float(io, op.y.value) end function svg_print_path_op(io::IO, op::VertLineRelPathOp) print(io, 'v') svg_print_float(io, op.Δy.value) end function svg_print_path_op(io::IO, op::CubicCurveAbsPathOp) print(io, 'C') svg_print_float(io, op.ctrl1[1].value) print(io, ' ') svg_print_float(io, op.ctrl1[2].value) print(io, ' ') svg_print_float(io, op.ctrl2[1].value) print(io, ' ') svg_print_float(io, op.ctrl2[2].value) print(io, ' ') svg_print_float(io, op.to[1].value) print(io, ' ') svg_print_float(io, op.to[2].value) end function svg_print_path_op(io::IO, op::CubicCurveRelPathOp) print(io, 'c') svg_print_float(io, op.ctrl1[1].value) print(io, ' ') svg_print_float(io, op.ctrl1[2].value) print(io, ' ') svg_print_float(io, op.ctrl2[1].value) print(io, ' ') svg_print_float(io, op.ctrl2[2].value) print(io, ' ') svg_print_float(io, op.to[1].value) print(io, ' ') svg_print_float(io, op.to[2].value) end function svg_print_path_op(io::IO, op::CubicCurveShortAbsPathOp) print(io, 'S') svg_print_float(io, op.ctrl2[1].value) print(io, ' ') svg_print_float(io, op.ctrl2[2].value) print(io, ' ') svg_print_float(io, op.to[1].value) print(io, ' ') svg_print_float(io, op.to[2].value) end function svg_print_path_op(io::IO, op::CubicCurveShortRelPathOp) print(io, 's') svg_print_float(io, op.ctrl2[1].value) print(io, ' ') svg_print_float(io, op.ctrl2[2].value) print(io, ' ') svg_print_float(io, op.to[1].value) print(io, ' ') svg_print_float(io, op.to[2].value) end function svg_print_path_op(io::IO, op::QuadCurveAbsPathOp) print(io, 'Q') svg_print_float(io, op.ctrl1[1].value) print(io, ' ') svg_print_float(io, op.ctrl1[2].value) print(io, ' ') svg_print_float(io, op.to[1].value) print(io, ' ') svg_print_float(io, op.to[2].value) end function svg_print_path_op(io::IO, op::QuadCurveRelPathOp) print(io, 'q') svg_print_float(io, op.ctrl1[1].value) print(io, ' ') svg_print_float(io, op.ctrl1[2].value) print(io, ' ') svg_print_float(io, op.to[1].value) print(io, ' ') svg_print_float(io, op.to[2].value) end function svg_print_path_op(io::IO, op::QuadCurveShortAbsPathOp) print(io, 'T') svg_print_float(io, op.to[1].value) print(io, ' ') svg_print_float(io, op.to[2].value) end function svg_print_path_op(io::IO, op::QuadCurveShortRelPathOp) print(io, 't') svg_print_float(io, op.to[1].value) print(io, ' ') svg_print_float(io, op.to[2].value) end function svg_print_path_op(io::IO, op::ArcAbsPathOp) print(io, 'A') svg_print_float(io, op.rx.value) print(io, ' ') svg_print_float(io, op.ry.value) print(io, ' ') svg_print_float(io, op.rotation) print(io, ' ', op.largearc ? 1 : 0, ' ', op.sweep ? 1 : 0, ' ') svg_print_float(io, op.to[1].value) print(io, ' ') svg_print_float(io, op.to[2].value) end function svg_print_path_op(io::IO, op::ArcRelPathOp) print(io, 'a') svg_print_float(io, op.rx.value) print(io, ' ') svg_print_float(io, op.ry.value) print(io, ' ') svg_print_float(io, op.rotation) print(io, ' ', op.largearc ? 1 : 0, ' ', op.sweep ? 1 : 0, ' ') svg_print_float(io, op.to[1].value) print(io, ' ') svg_print_float(io, op.to[2].value) end function draw(img::SVG, prim::PathPrimitive, idx::Int) indent(img) print(img.out, "\n") end ================================================ FILE: src/batch.jl ================================================ """ A form batch is a vectorized form with n primitives transformed into a simpler representation: one primitive repositioned n times. On certain backends this leads to more efficient drawing. For example, SVG can be shortened by using and tags, and raster graphics can render the form primitive to a back buffer and blit it into place for faster drawing. Batching is an optimization transform that happens at draw time. There's currently no mechanism to manually batch. E.g. contexts cannot have FormBatch children. """ struct FormBatch{P <: FormPrimitive} primitive::P offsets::Vector{AbsoluteVec2} end """ Attempt to batch a form. Return a Nothing singleton if the Form could not be batched, and FormBatch object if the original form can be replaced. """ batch(form::Form{P}) where P = nothing # Note: in tests using random data, this optimization wasn't worth it. I'm # keeping it around out of hopes I find a more clever version that is # worthwhile, or benchmarks using real data show different results. # maximum distance between offsets to be considered redundand in mm const offset_redundancy_threshold = 0.05 """ Produce a new array of offsets in which near duplicate values have been removed. """ function filter_redundant_offsets!(offsets::Vector{AbsoluteVec2}) isempty(offsets) && return offsets sort!(offsets) nonredundant_offsets = AbsoluteVec2[offsets[1]] for i in 2:length(offsets) # use l1 distance for perf d = abs(offsets[i-1][1].value - offsets[i][1].value) + abs(offsets[i-1][2].value - offsets[i][2].value) d > offset_redundancy_threshold && push!(nonredundant_offsets, offsets[i]) end @show (length(offsets), length(nonredundant_offsets)) return nonredundant_offsets end #= function batch{T <: CirclePrimitive}(form::Form{T}) # circles can be batched if they all have the same radius. r = form.primitives[1].radius n = length(form.primitives) for i in 2:n form.primitives[i].radius == r || return Nothing end prim = CirclePrimitive((0mm, 0mm), r) offsets = Array{AbsoluteVec2}(n) for i in 1:n offsets[i] = form.primitives[i].center end return FormBatch(prim, offsets) end =# # TODO: same for polygon, rectangle, ellipse # TODO: batch needs to be exposed as something that users can construct and # insert into a context. It doesn't make sense to make the same polygon over and # over and then try to convert it to FormBach. # Don't attempt to optimize for batching if the form is smaller than this. const batch_length_threshold = 100 """ Count the number of unique primitives in a property, stopping when max_count is exceeded. """ function count_unique_primitives(property::Property, max_count::Int) unique_primitives = Set{eltype(property.primitives)}() for primitive in property.primitives push!(unique_primitives, primitive) length(unique_primitives) > max_count && break end return length(unique_primitives) end """ Remove and return vector forms and vector properties from the Context. """ function excise_vector_children!(ctx::Context) # excise vector forms prev_form_child = form_child = ctx.form_children forms = Form[] while !isa(form_child, ListNull) if length(form_child.head.primitives) > 1 push!(forms, form_child.head) if prev_form_child == form_child prev_form_child = ctx.form_children = form_child.tail else prev_form_child.tail = form_child.tail end else prev_form_child = form_child end form_child = form_child.tail end # excise vector properties prev_property_child = property_child = ctx.property_children properties = Property[] while !isa(property_child, ListNull) if length(property_child.head.primitives) > 1 push!(properties, property_child.head) if prev_property_child == property_child prev_property_child = ctx.property_children = property_child.tail else prev_property_child.tail = property_child.tail end else prev_property_child = property_child end property_child = property_child.tail end return (forms, properties) end """ Attempt to transform a tree into an equivalent tree that can more easily be batched. What this does is look for patterns in which a long vector form is accompanied by a large vector property that has a relatively small number of unique values. If there are n unique values, we can split it into n contexts, each with a shorter vector form and only scalar properties. """ function optimize_batching(ctx::Context) # condition 1: has a 1 or more long vector forms max_form_length = 0 form_child = ctx.form_children while !isa(form_child, ListNull) max_form_length = max(max_form_length, length(form_child.head.primitives)) form_child = form_child.tail end max_form_length < batch_length_threshold && return ctx # condition 2: has a 1 or more long vector properties each with a smaller # number of unique values max_count = div(max_form_length, batch_length_threshold) + 1 max_unique_primitives = 0 prop_child = ctx.property_children while !isa(prop_child, ListNull) if length(prop_child.head.primitives) > 1 max_unique_primitives = max(max_unique_primitives, count_unique_primitives(prop_child.head, max_count)) end prop_child = prop_child.tail end # don't batch when there are not many forms per unique property primitive if max_unique_primitives == 0 || div(max_form_length, max_unique_primitives) + 1 < batch_length_threshold return ctx end # non-destructive since this happens at draw time and draw should not modify # the context. ctx = copy(ctx) # step 1: remove vector form and vector properties forms, properties = excise_vector_children!(ctx) # step 2: split primitives into groups on the cross product of property # primives n = length(forms[1].primitives) grouped_forms = Dict{UInt, Vector{Form}}() grouped_properties = Dict{UInt, Vector{Property}}() for i in 1:n h = UInt(0) for property in properties h = hash(property.primitives[i], h) end if !haskey(grouped_forms, h) grouped_forms[h] = Form[similar(form) for form in forms] group_prop = Array{Property}(undef, length(properties)) for j in 1:length(properties) group_prop[j] = Property([properties[j].primitives[i]]) end grouped_properties[h] = group_prop end for j in 1:length(forms) push!(grouped_forms[h][j].primitives, forms[j].primitives[i]) end end # step 3: put forms in new contexts and insert into the ctx for (h, fs) in grouped_forms subctx = context() compose!(subctx, fs...) compose!(subctx, grouped_properties[h]...) compose!(ctx, subctx) end return ctx end ================================================ FILE: src/cairo_backends.jl ================================================ import .Cairo: CairoContext, CairoSurface, CairoARGBSurface, CairoEPSSurface, CairoPDFSurface, CairoSVGSurface, CairoImageSurface abstract type ImageBackend end abstract type PNGBackend <: ImageBackend end abstract type VectorImageBackend <: ImageBackend end abstract type SVGBackend <: VectorImageBackend end abstract type PDFBackend <: VectorImageBackend end abstract type PSBackend <: VectorImageBackend end abstract type CairoBackend <: VectorImageBackend end mutable struct ImagePropertyState stroke::RGBA{Float64} fill::RGBA{Float64} stroke_dash::Array{Float64,1} stroke_linecap::LineCap stroke_linejoin::LineJoin visible::Bool linewidth::AbsoluteLength fontsize::AbsoluteLength font::AbstractString clip::Union{ClipPrimitive, Nothing} arrow::Bool end mutable struct ImagePropertyFrame # Vector properties in this frame. vector_properties::Dict{Type, Property} # True if this property frame has scalar properties. Scalar properties are # emitted as a group ( tag) that must be closed when the frame is popped. has_scalar_properties::Bool end ImagePropertyFrame() = ImagePropertyFrame(Dict{Type, Property}(), false) mutable struct Image{B<:ImageBackend} <: Backend out::IO surface::CairoSurface ctx::CairoContext width::Float64 height::Float64 # Current state stroke::RGBA{Float64} fill::RGBA{Float64} stroke_dash::Array{Float64,1} stroke_linecap::LineCap stroke_linejoin::LineJoin visible::Bool linewidth::AbsoluteLength fontsize::AbsoluteLength font::AbstractString clip::Union{ClipPrimitive, Nothing} arrow::Bool # Keep track of property state_stack::Vector{ImagePropertyState} property_stack::Vector{ImagePropertyFrame} vector_properties::Dict{Type, Union{Property, Nothing}} # Close the surface when finished owns_surface::Bool # Backend is responsible for opening/closing the file ownedfile::Bool # Filename when ownedfile is true filename::Union{AbstractString, Nothing} # True when finish has been called and no more drawing should occur finished::Bool # Emit on finish emit_on_finish::Bool # Points (or pixels for PNG) per mm ppmm::Float64 # For use with the t/T and s/S commands in SVG-style paths last_ctrl1_point::Union{AbsoluteVec2, Nothing} last_ctrl2_point::Union{AbsoluteVec2, Nothing} end function Image{B}(surface::CairoSurface, ctx::CairoContext, out::IO; width = 0, height = 0, stroke = default_stroke_color == nothing ? RGBA{Float64}(0, 0, 0, 0) : convert(RGBA{Float64}, default_stroke_color), fill = default_fill_color == nothing ? RGBA{Float64}(0, 0, 0, 0) : convert(RGBA{Float64}, default_fill_color), stroke_dash = [], stroke_linecap = LineCapButt(), stroke_linejoin = LineJoinMiter(), visible = true, linewidth = default_line_width, fontsize = default_font_size, font = default_font_family, clip = nothing, arrow = false, state_stack = Array{ImagePropertyState}(undef, 0), property_stack = Array{ImagePropertyFrame}(undef, 0), vector_properties = Dict{Type, Union{Property, Nothing}}(), owns_surface = false, ownedfile = false, filename = nothing, finished = false, emit_on_finish = false, ppmm = 72 / 25.4, last_ctrl1_point = nothing, last_ctrl2_point = nothing) where B<:ImageBackend Image{B}(out, surface, ctx, width, height, stroke, fill, stroke_dash, stroke_linecap, stroke_linejoin, visible, linewidth, fontsize, font, clip, arrow, state_stack, property_stack, vector_properties, owns_surface, ownedfile, filename, finished, emit_on_finish, ppmm, last_ctrl1_point, last_ctrl2_point) end Image{B}(surface::CairoSurface, ctx::CairoContext) where {B<:ImageBackend} = Image{B}(surface, ctx, IOBuffer()) Image{B}(surface::CairoSurface) where {B<:ImageBackend} = Image{B}(surface, CairoContext(surface)) function Image{B}(out::IO, width::MeasureOrNumber=default_graphic_width, height::MeasureOrNumber=default_graphic_height, emit_on_finish::Bool=true; dpi = (B==PNGBackend ? 96 : 72), kwargs...) where B<:ImageBackend width = size_measure(width) height = size_measure(height) (!isa(width, AbsoluteLength) || !isa(height, AbsoluteLength)) && error("Image size must be specificed in absolute units.") ppmm = dpi / 25.4 width = width.value * ppmm height = height.value * ppmm surface = newsurface(B, out, width, height) Image{B}(surface, CairoContext(surface), out; width=width, height=height, owns_surface=true, emit_on_finish=emit_on_finish, ppmm=ppmm, kwargs...) end Image{B}(filename::AbstractString, width::MeasureOrNumber=default_graphic_width, height::MeasureOrNumber=default_graphic_height; dpi = (B==PNGBackend ? 96 : 72)) where {B<:ImageBackend} = Image{B}(open(filename, "w"), width, height, dpi=dpi; ownedfile=true, filename=filename) Image{B}(width::MeasureOrNumber=default_graphic_width, height::MeasureOrNumber=default_graphic_height, emit_on_finish::Bool=true; dpi = (B==PNGBackend ? 96 : 72)) where {B<:ImageBackend} = Image{B}(IOBuffer(), width, height, emit_on_finish, dpi=dpi) docfunc(func,abbr) = """ $func([output::Union{AbstractString, IO}], width=√200cm, height=10cm; dpi=$(func==:PNG ? 96 : 72)) -> Backend Create a $abbr backend. The output is normally passed to [`draw`](@ref). Specify a filename using a string as the first argument. Depends on `Cairo.jl`. # Examples ``` using Cairo c = compose(context(), circle()) draw($(func)("myplot.$(lowercase(String(func)))", 10cm, 5cm, dpi=250), c) ``` """ for (func,abbr) in [(:PNG,"Portable Network Graphics"), (:PDF,"Portable Document Format"), (:PS,"Postscript")] backend = Symbol(func, "Backend") docstr = docfunc(func,abbr) @eval $func(args...; kwargs...) = Image{$backend}(args...; kwargs...) @eval @doc $docstr $func end const CAIROSURFACE = Image{CairoBackend} function (img::Image)(x) draw(img, x) end function canbatch(img::Image) for vp in values(img.vector_properties) (vp === nothing) || return false end return true end # convert compose absolute units (millimeters) to the absolute units used by the # cairo surface (pixels for PNG, points for all others) absolute_native_units(img::Image, u::Float64) = img.ppmm * u surface(img::Image) = img.surface Measures.width(img::Image) = (Cairo.width(img.surface) / img.ppmm) * mm Measures.height(img::Image) = (Cairo.height(img.surface) / img.ppmm) * mm iswithjs(img::Image) = false iswithousjs(img::Image) = true finish(::Type{B}, img::Image) where {B<:ImageBackend} = nothing finish(::Type{PNGBackend}, img::Image) = Cairo.write_to_png(img.surface, img.out) function finish(img::Image{B}) where B<:ImageBackend img.finished && return img.owns_surface && Cairo.destroy(img.ctx) finish(B, img) img.owns_surface && Cairo.destroy(img.surface) img.finished = true img.emit_on_finish && typeof(img.out) == IOBuffer && display(img) hasmethod(flush, (typeof(img.out),)) && flush(img.out) img.ownedfile && close(img.out) end function newsurface(::Type{B}, out, width, height) where B local surface::CairoSurface if B == SVGBackend surface = CairoSVGSurface(out, width, height) elseif B == PNGBackend surface = CairoARGBSurface(round(Integer, width), round(Integer, height)) elseif B == PDFBackend surface = CairoPDFSurface(out, width, height) elseif B == PSBackend surface = CairoEPSSurface(out, width, height) elseif B == CairoBackend surface = out else error("Unkown Cairo backend.") end Cairo.status(surface) == Cairo.STATUS_SUCCESS || error("Unable to create cairo surface.") surface end function reset(img::Image{B}) where B img.owns_surface || error("Backend can't be reused since an external cairo surface is being used.") if img.ownedfile img.out = open(img.filename, "w") else try seekstart(img.out) catch error("Backend can't be reused, since the output stream is not seekable.") end end img.surface = newsurface(B, img.out, img.width, img.height) img.ctx = CairoContext(img.surface) img.finished = false end isfinished(img::Image) = img.finished root_box(img::Image) = BoundingBox(width(img), height(img)) show(io::IO, ::MIME"image/png", img::Image{PNGBackend}) = write(io, String(take!(img.out))) show(io::IO, ::MIME"application/pdf", img::Image{PDFBackend}) = write(io, String(take!(img.out))) show(io::IO, ::MIME"application/postscript", img::Image{PSBackend}) = write(io, String(take!(img.out))) # Applying Properties # ------------------- function push_property_frame(img::Image, properties::Vector{Property}) isempty(properties) && return frame = ImagePropertyFrame() isp = isscalar.(properties) scalar_properties = Dict{Type, Property}(typeof(x)=>x for x in properties[isp]) vector_properties = Dict{Type, Property}(typeof(x)=>x for x in properties[.!isp]) kt = [Property{FillOpacityPrimitive}, Property{FillPrimitive}] if haskey(scalar_properties, kt[1]) && haskey(vector_properties, kt[2]) alpha = scalar_properties[kt[1]].primitives[1].value vector_properties[kt[1]] = fillopacity(fill(alpha, length(vector_properties[kt[2]].primitives))) pop!(scalar_properties, kt[1]) end frame.has_scalar_properties = !isempty(scalar_properties) img.vector_properties = vector_properties frame.vector_properties = vector_properties push!(img.property_stack, frame) isempty(scalar_properties) && return save_property_state(img) for (_, property) in scalar_properties apply_property(img, property.primitives[1]) end haskey(scalar_properties, kt[1]) && apply_property(img, scalar_properties[kt[1]].primitives[1]) end function pop_property_frame(img::Image) @assert !isempty(img.property_stack) frame = pop!(img.property_stack) frame.has_scalar_properties && restore_property_state(img) for (propertytype, property) in frame.vector_properties img.vector_properties[propertytype] = nothing for i in length(img.property_stack):-1:1 if haskey(img.property_stack[i].vector_properties, propertytype) img.vector_properties[propertytype] = img.property_stack[i].vector_properties[propertytype] end end end end function save_property_state(img::Image) push!(img.state_stack, ImagePropertyState( img.stroke, img.fill, img.stroke_dash, img.stroke_linecap, img.stroke_linejoin, img.visible, img.linewidth, img.fontsize, img.font, img.clip, img.arrow)) Cairo.save(img.ctx) end function restore_property_state(img::Image) state = pop!(img.state_stack) img.stroke = state.stroke img.fill = state.fill img.stroke_dash = state.stroke_dash img.stroke_linecap = state.stroke_linecap img.stroke_linejoin = state.stroke_linejoin img.visible = state.visible img.linewidth = state.linewidth img.fontsize = state.fontsize img.font = state.font img.clip = state.clip img.arrow = state.arrow Cairo.restore(img.ctx) end # Return true if the vector properties need to be pushed and popped, rather # than simply applied. function vector_properties_require_push_pop(img::Image) for (propertytype, property) in img.vector_properties in(propertytype, [Property{FontPrimitive}, Property{FontSizePrimitive}, Property{ClipPrimitive}]) && return true end return false end function push_vector_properties(img::Image, idx::Int) save_property_state(img) for (propertytype, property) in img.vector_properties (property === nothing) && continue primitives = property.primitives idx > length(primitives) && error("Vector form and vector property differ in length. Can't distribute.") apply_property(img, primitives[idx]) end end pop_vector_properties(img::Image) = restore_property_state(img) apply_property(img::Image, p::StrokePrimitive) = img.stroke = p.color apply_property(img::Image, p::FillPrimitive) = img.fill = p.color apply_property(img::Image, p::FillOpacityPrimitive) = img.fill = RGBA{Float64}(color(img.fill), p.value) apply_property(img::Image, p::StrokeOpacityPrimitive) = img.stroke = RGBA{Float64}(color(img.stroke), p.value) apply_property(img::Image, p::StrokeDashPrimitive) = img.stroke_dash = map(v -> absolute_native_units(img, v.value), p.value) apply_property(img::Image, p::StrokeLineCapPrimitive) = img.stroke_linecap = p.value apply_property(img::Image, p::StrokeLineJoinPrimitive) = img.stroke_linejoin = p.value apply_property(img::Image, p::VisiblePrimitive) = img.visible = p.value function apply_property(img::Image, property::LineWidthPrimitive) img.linewidth = property.value Cairo.set_line_width(img.ctx, absolute_native_units(img, property.value.value)) end function apply_property(img::Image, property::FontPrimitive) img.font = property.family font_desc = ccall((:pango_layout_get_font_description, Cairo.libpango), Ptr{Cvoid}, (Ptr{Cvoid},), img.ctx.layout) if font_desc == C_NULL size = absolute_native_units(img, default_font_size.value) else size = ccall((:pango_font_description_get_size, Cairo.libpango), Cint, (Ptr{Cvoid},), font_desc) end Cairo.set_font_face(img.ctx, @sprintf("%s %0.2fpx", property.family, size / PANGO_SCALE)) end function apply_property(img::Image, property::FontSizePrimitive) img.fontsize = property.value font_desc = ccall((:pango_layout_get_font_description, Cairo.libpango), Ptr{Cvoid}, (Ptr{Cvoid},), img.ctx.layout) if font_desc == C_NULL family = "sans" else family = ccall((:pango_font_description_get_family, Cairo.libpango), Ptr{UInt8}, (Ptr{Cvoid},), font_desc) family = unsafe_string(family) end Cairo.set_font_face(img.ctx, @sprintf("%s %.2fpx", family, absolute_native_units(img, property.value.value))) end function apply_property(img::Image, property::ClipPrimitive) if isempty(property.points); return; end move_to(img, property.points[1]) for point in property.points[2:end] line_to(img, point) end close_path(img) Cairo.clip(img.ctx) img.clip = property end # No-op SVG+JS only properties apply_property(img::Image, property::JSIncludePrimitive) = nothing apply_property(img::Image, property::JSCallPrimitive) = nothing apply_property(img::Image, property::SVGIDPrimitive) = nothing apply_property(img::Image, property::SVGClassPrimitive) = nothing apply_property(img::Image, property::SVGAttributePrimitive) = nothing # Cairo Wrappers # -------------- function current_point(img::Image) x = Array{Float64}(undef, 1) y = Array{Float64}(undef, 1) ccall((:cairo_get_current_point, Cairo.libcairo), Cvoid, (Ptr{Cvoid}, Ptr{Float64}, Ptr{Float64}), img.ctx.ptr, x, y) return ((x[1] / img.ppmm)*mm, (x[2] / img.ppmm)*mm) end move_to(img::Image, point::AbsoluteVec2) = Cairo.move_to(img.ctx, absolute_native_units(img, point[1].value), absolute_native_units(img, point[2].value)) rel_move_to(img::Image, point::AbsoluteVec2) = Cairo.rel_move_to(img.ctx, absolute_native_units(img, point[1].value), absolute_native_units(img, point[2].value)) line_to(img::Image, point::AbsoluteVec2) = Cairo.line_to(img.ctx, absolute_native_units(img, point[1].value), absolute_native_units(img, point[2].value)) rel_line_to(img::Image, point::AbsoluteVec2) = Cairo.rel_line_to(img.ctx, absolute_native_units(img, point[1].value), absolute_native_units(img, point[2].value)) rel_curve_to(img::Image, ctrl1::AbsoluteVec2, ctrl2::AbsoluteVec2, to::AbsoluteVec2) = Cairo.rel_curve_to(img.ctx, absolute_native_units(img, ctrl1[1].value), absolute_native_units(img, ctrl1[2].value), absolute_native_units(img, ctrl2[1].value), absolute_native_units(img, ctrl2[2].value), absolute_native_units(img, to[1].value), absolute_native_units(img, to[2].value),) rectangle(img::Image, corner::AbsoluteVec2, width::AbsoluteLength, height::AbsoluteLength) = Cairo.rectangle(img.ctx, absolute_native_units(img, corner[1].value), absolute_native_units(img, corner[2].value), absolute_native_units(img, width.value), absolute_native_units(img, height.value)) circle(img::Image, center::AbsoluteVec2, radius::AbsoluteLength) = Cairo.circle(img.ctx, absolute_native_units(img, center[1].value), absolute_native_units(img, center[2].value), absolute_native_units(img, radius.value)) curve_to(img::Image, ctrl1::AbsoluteVec2, ctrl2::AbsoluteVec2, anchor::AbsoluteVec2) = Cairo.curve_to(img.ctx, absolute_native_units(img, ctrl1[1].value), absolute_native_units(img, ctrl1[2].value), absolute_native_units(img, ctrl2[1].value), absolute_native_units(img, ctrl2[2].value), absolute_native_units(img, anchor[1].value), absolute_native_units(img, anchor[2].value)) close_path(img::Image) = Cairo.close_path(img.ctx) new_sub_path(img::Image) = Cairo.new_sub_path(img.ctx) arc(img::Image, x::Float64, y::Float64, radius::Float64, angle1::Float64, angle2::Float64) = Cairo.arc(img.ctx, absolute_native_units(img, x), absolute_native_units(img, y), absolute_native_units(img, radius), angle1, angle2) arc_negative(img::Image, x::Float64, y::Float64, radius::Float64, angle1::Float64, angle2::Float64) = Cairo.arc_negative(img.ctx, absolute_native_units(img, x), absolute_native_units(img, y), absolute_native_units(img, radius), angle1, angle2) translate(img::Image, tx::Float64, ty::Float64) = Cairo.translate(img.ctx, absolute_native_units(img, tx), absolute_native_units(img, ty)) scale(img::Image, sx::Float64, sy::Float64) = Cairo.scale(img.ctx, sx, sy) rotate(img::Image, theta::Float64) = Cairo.rotate(img.ctx, theta) function rotate(img::Image, theta::Float64, x::Float64, y::Float64) ct = cos(theta) st = sin(theta) x′ = x - (ct * x - st * y) y′ = y - (st * x + ct * y) translate(img, x′, y′) rotate(img, theta) end # Convert native linecap/linejoin enums to the Cairo values. cairo_linecap(::LineCapButt) = Cairo.CAIRO_LINE_CAP_BUTT cairo_linecap(::LineCapRound) = Cairo.CAIRO_LINE_CAP_ROUND cairo_linecap(::LineCapSquare) = Cairo.CAIRO_LINE_CAP_SQUARE cairo_linejoin(::LineJoinMiter) = Cairo.CAIRO_LINE_JOIN_MITER cairo_linejoin(::LineJoinBevel) = Cairo.CAIRO_LINE_JOIN_BEVEL cairo_linejoin(::LineJoinRound) = Cairo.CAIRO_LINE_JOIN_ROUND function fillstroke(img::Image, strokeonly::Bool=false) img.visible || return if img.fill.alpha > 0.0 && !strokeonly Cairo.set_source_rgba(img.ctx, img.fill.r, img.fill.g, img.fill.b, img.fill.alpha) if img.stroke.alpha > 0.0 Cairo.fill_preserve(img.ctx) else Cairo.fill(img.ctx) end end if img.stroke.alpha > 0.0 Cairo.set_source_rgba(img.ctx, img.stroke.r, img.stroke.g, img.stroke.b, img.stroke.alpha) Cairo.set_dash(img.ctx, img.stroke_dash) Cairo.set_line_cap(img.ctx, cairo_linecap(img.stroke_linecap)) Cairo.set_line_join(img.ctx, cairo_linejoin(img.stroke_linejoin)) Cairo.stroke(img.ctx) end # if the path wasn't stroked or filled, we should still clear it Cairo.new_path(img.ctx) end # Form Drawing # ------------ function draw(img::Image, form::Form) if vector_properties_require_push_pop(img) for (idx, primitive) in enumerate(form.primitives) push_vector_properties(img, idx) draw(img, primitive) pop_vector_properties(img) end else for (idx, primitive) in enumerate(form.primitives) for (propertytype, property) in img.vector_properties (property === nothing) && continue primitives = property.primitives idx > length(primitives) && error("Vector form and vector property differ in length. Can't distribute.") apply_property(img, primitives[idx]) end kt = [Property{FillOpacityPrimitive}] haskey(img.vector_properties, kt[1]) && apply_property(img, img.vector_properties[kt[1]].primitives[idx]) draw(img, primitive) end end end function draw(img::Image, prim::RectanglePrimitive) rectangle(img, prim.corner, prim.width, prim.height) fillstroke(img) end function draw(img::Image, prim::PolygonPrimitive) length(prim.points) <= 1 && return prev_ok = false for (i,p) in enumerate(prim.points) ok = isfinite(p[1].value) && isfinite(p[2].value) if ok && prev_ok line_to(img, p) elseif !ok && prev_ok close_path(img) fillstroke(img) else move_to(img, p) end prev_ok = ok end close_path(img) fillstroke(img) end function draw(img::Image, prim::Compose.ComplexPolygonPrimitive) isempty(prim.rings) && return for ring in prim.rings move_to(img, ring[1]) for point in ring[2:end] line_to(img, point) end close_path(img) end fillstroke(img) end function draw(img::Image, prim::CirclePrimitive) new_sub_path(img) circle(img, prim.center, prim.radius) fillstroke(img) end function draw(img::Image, prim::EllipsePrimitive) new_sub_path(img) cx = prim.center[1].value cy = prim.center[2].value rx = sqrt((prim.x_point[1].value - cx)^2 + (prim.x_point[2].value - cy)^2) ry = sqrt((prim.y_point[1].value - cx)^2 + (prim.y_point[2].value - cy)^2) theta = atan(prim.x_point[2].value - cy, prim.x_point[1].value - cx) all(isfinite,[cx, cy, rx, ry, theta]) || return save_property_state(img) translate(img, cx, cy) rotate(img, theta) translate(img, -rx, -ry) scale(img, 2rx, 2ry) arc(img, 0.5, 0.5, 0.5, 0.0, 2pi) restore_property_state(img) fillstroke(img) end function draw(img::Image, prim::LinePrimitive) length(prim.points) <= 1 && return prev_ok = false for (i,p) in enumerate(prim.points) ok = isfinite(p[1].value) && isfinite(p[2].value) if ok && prev_ok line_to(img, p) else move_to(img, p) end prev_ok = ok end img.arrow && arrow(img, prim.points) fillstroke(img, true) end function get_layout_size(img::Image) width, height = Cairo.get_layout_size(img.ctx) return ((width / img.ppmm) * mm, (height / img.ppmm) * mm) end show(io::IO, ::MIME"image/png", ctx::Context) = draw(PNG(io, default_graphic_width, default_graphic_height), ctx) function draw(img::Image, prim::TextPrimitive) (!img.visible || (img.fill.alpha == 0.0 && img.stroke.alpha == 0.0)) && return Cairo.set_text(img.ctx, prim.value, true) width, height = get_layout_size(img) pos = (prim.position[1]+prim.offset[1], prim.position[2]+prim.offset[2] - height) if prim.halign != hleft || prim.valign != vbottom if prim.halign == hcenter pos = (pos[1] - width/2, pos[2]) elseif prim.halign == hright pos = (pos[1] - width, pos[2]) end if prim.valign == vcenter pos = (pos[1], pos[2] + height/2) elseif prim.valign == vtop pos = (pos[1], pos[2] + height) end end Cairo.set_source_rgba(img.ctx, img.fill.r, img.fill.g, img.fill.b, img.fill.alpha) if prim.rot.theta != 0.0 save_property_state(img) rotate(img, prim.rot.theta, prim.rot.offset[1].value, prim.rot.offset[2].value) end move_to(img, pos) Cairo.show_layout(img.ctx) prim.rot.theta == 0.0 || restore_property_state(img) end function draw(img::Image, prim::CurvePrimitive) move_to(img, prim.anchor0) curve_to(img, prim.ctrl0, prim.ctrl1, prim.anchor1) fillstroke(img, true) end draw(img::Image, prim::BitmapPrimitive) = error("Embedding bitmaps in Cairo backends (i.e. PNG, PDF, PS) is not supported.") function draw(img::Image, batch::FormBatch) bounds = boundingbox(batch.primitive, img.linewidth, img.font, img.fontsize) width = bounds.a[1].value * img.ppmm height = bounds.a[2].value * img.ppmm xt = -bounds.x0[1].value * img.ppmm yt = -bounds.x0[2].value * img.ppmm Cairo.save(img.ctx) Cairo.reset_clip(img.ctx) Cairo.translate(img.ctx, xt, yt) Cairo.push_group(img.ctx) draw(img, batch.primitive) pattern = Cairo.pop_group(img.ctx) surface = Cairo.pattern_get_surface(pattern) Cairo.restore(img.ctx) # reapply the clipping region we just reset if img.clip !== nothing apply_property(img, img.clip) end Cairo.set_antialias(img.ctx, Cairo.ANTIALIAS_NONE) for offset in batch.offsets x = offset[1].value * img.ppmm - xt y = offset[2].value * img.ppmm - yt Cairo.set_source_surface(img.ctx, surface, x, y) Cairo.rectangle(img.ctx, x, y, width, height) Cairo.fill(img.ctx) end Cairo.set_antialias(img.ctx, Cairo.ANTIALIAS_DEFAULT) end function draw(img::Compose.Image, prim::ArcPrimitive) new_sub_path(img) xc = prim.center[1] yc = prim.center[2] prim.sector && move_to(img, (xc, yc)) arc(img, xc.value, yc.value, prim.radius.value, prim.angle1, prim.angle2) prim.sector && line_to(img, (xc, yc)) img.arrow && arrow(img, prim) fillstroke(img) end # Arrows apply_property(img::Image, p::ArrowPrimitive) = img.arrow = p.value function arrow(img::Image, points::Vector{<:Vec}) xmax, ymax = points[end] dxy = points[end] .- points[end-1] dx, dy = dxy[1].value, dxy[2].value vl, θ = 0.225*hypot(dy,dx), atan(dy, dx) arrowhead(img, (xmax,ymax), vl, θ) end function arrow(img::Image, prim::ArcPrimitive) xy = prim.center .+ prim.radius.*(cos(prim.angle2), sin(prim.angle2)) θ = prim.angle2 + 0.45π arrowhead(img, xy, 0.5*prim.radius.value, θ) end function arrowhead(img::Image, point::Vec, vl::Float64, θ::Float64) xmax, ymax = point ϕ = pi/15 xr = -vl*[cos(θ+ϕ), cos(θ-ϕ)].*1mm yr = -vl*[sin(θ+ϕ), sin(θ-ϕ)].*1mm move_to(img, (xmax+xr[1], ymax+yr[1])) line_to(img, (xmax, ymax)) line_to(img, (xmax+xr[2], ymax+yr[2])) end # Bezigon function draw(img::Image, prim::BezierPolygonPrimitive) move_to(img, prim.anchor) currentpoint = prim.anchor for side in prim.sides if length(side)==1 line_to(img, side[1]) elseif length(side)==2 cp1 = controlpnt(currentpoint, side[1]) cp2 = controlpnt(side[2], side[1]) curve_to(img, cp1, cp2, side[2]) elseif length(side)==3 curve_to(img, side[1], side[2], side[3]) end currentpoint = side[end] end close_path(img) fillstroke(img) end ================================================ FILE: src/container.jl ================================================ # A container is a node in the tree that can have Forms, Properties, or other # Containers as children. abstract type Container <: ComposeNode end # The basic Container which defines a coordinate transform for its children. mutable struct Context <: Container # Bounding box relative to the parent's coordinates box::BoundingBox units::Union{UnitBox, Nothing} rot::Union{Rotation, Nothing} mir::Union{Mirror, Nothing} shear::Union{Shear, Nothing} # Container children container_children::List{Container} form_children::List{Form} property_children::List{Property} order::Int clip::Bool withjs::Bool withoutjs::Bool raster::Bool minwidth::Maybe(Float64) minheight::Maybe(Float64) # A field that can be used by layouts to indicate that one configuration is # preferable to another. penalty::Float64 tag::Symbol function Context(box, units, rotation, mirror, shear, container_children, form_children, property_children, order, clip, withjs, withoutjs, raster, minwidth, minheight, penalty, tag) if isa(minwidth, AbsoluteLength) minwidth = minwidth.value end if isa(minheight, AbsoluteLength) minheight = minheight.value end return new(box, units, rotation, mirror, shear, container_children, form_children, property_children, order, clip, withjs, withoutjs, raster, minwidth, minheight, penalty, tag) end end Context(x0=0.0w, y0=0.0h, width=1.0w, height=1.0h; units=nothing, rotation=nothing, mirror=nothing, shear=nothing, order=0, clip=false, withjs=false, withoutjs=false, raster=false, minwidth=nothing, minheight=nothing, penalty=0.0, tag=empty_tag) = Context(BoundingBox(x0, y0, width, height), units, rotation, mirror, ListNull{ComposeNode}(), order, clip, withjs, withoutjs, raster, minwidth, minheight, penalty, tag) Context(ctx::Context) = Context(ctx.box, ctx.units, ctx.rot, ctx.mir, ctx.shear, ctx.container_children, ctx.form_children, ctx.property_children, ctx.order, ctx.clip, ctx.withjs, ctx.withoutjs, ctx.raster, ctx.minwidth, ctx.minheight, ctx.penalty, ctx.tag) """ context(x0=0.0w, y0=0.0h, width=1.0w, height=1.0h; units=nothing, rotation=nothing, mirror=nothing, shear=nothing, withjs=false, withoutjs=false, minwidth=nothing, minheight=nothing, order=0, clip=false, raster=false) -> Context Create a node in the tree structure that defines a graphic. Use [`compose`](@ref) to connect child nodes to a parent node. See also [`Rotation`](@ref), [`Mirror`](@ref), and [`Shear`](@ref). # Arguments - `units`: the coordinate system for the context, defined by a [`UnitBox`](@ref). - `order`: the Z-order of this context relative to its siblings. - `clip`: clip children of the canvas by its bounding box if true. - `withjs`: ignore this context and everything under it if we are not drawing to the SVGJS backend. - `withoutjs`: ignore this context if we *are* drawing on the SVGJS backend. - `raster`: if possible, render this subtree as a bitmap. This requires the Cairo. If Cairo isn't available, the default rendering is used. - `minwidth` and `minheight`: the minimum size needed to be drawn correctly, in millimeters. """ context(x0=0.0w, y0=0.0h, width=1.0w, height=1.0h; units=nothing, rotation=nothing, mirror=nothing, shear=nothing, order=0, clip=false, withjs=false, withoutjs=false, raster=false, minwidth=nothing, minheight=nothing, penalty=0.0, tag=empty_tag) = Context(BoundingBox(x_measure(x0), y_measure(y0), x_measure(width), y_measure(height)), units, rotation, mirror, shear, ListNull{Container}(), ListNull{Form}(), ListNull{Property}(), order, clip, withjs, withoutjs, raster, minwidth, minheight, penalty, tag) children(ctx::Context) = Iterators.flatten((ctx.container_children, ctx.form_children, ctx.property_children)) # Updating context fields set_units!(ctx::Context, units::UnitBox) = ctx.units = units copy(ctx::Context) = Context(ctx) iswithjs(ctx::Container) = ctx.withjs iswithoutjs(ctx::Container) = ctx.withoutjs order(cont::Container) = cont.order minwidth(cont::Container) = cont.minwidth minheight(cont::Container) = cont.minheight # Normalize cx or cy coordinate to a number [0,1] according to the given # unit box. function cx_proportion(from::Measure,units::UnitBox) cux0 = units.x0 cuw = units.width if cux0 == nothing cux0 = 0.0 cuw = 1.0 end ret_cx_proportion = from.cx != measure_nil ? (from.cx-cux0)/cuw : 0.0 if !isfinite(ret_cx_proportion) ret_cx_proportion = 0.0 end ret_cx_proportion end function cy_proportion(from::Measure,units::UnitBox) cuy0 = units.x0 cuh = units.width if cuy0 == nothing cuy0 = 0.0 cuh = 1.0 end ret_cy_proportion = from.cy != measure_nil ? (from.cy-cuy0)/cuh : 0.0 if !isfinite(ret_cy_proportion) ret_cy_proportion = 0.0 end ret_cy_proportion end function transformcoordinates(from::Measure, ctx::Context) Measure(;abs = from.abs) + (from.cw + cx_proportion(from,ctx.units))*ctx.box.width + (from.ch + cy_proportion(from,ctx.units))*ctx.box.height end function boundingbox(c::Context,linewidth::Measure=default_line_width, font::AbstractString=default_font_family, fontsize::Measure=default_font_size, parent_abs_width = nothing, parent_abs_height = nothing) for child in c.property_children for p in child.primitives if isa(p, LineWidthPrimitive) linewidth = p.value elseif isa(p, FontSizePrimitive) fontsize = p.value elseif isa(p, FontPrimitive) font = p.family end end end c_abs_width = c.box.width.abs if !iszero(c.box.width.cx) || !iszero(c.box.width.cy) || !iszero(c.box.width.cw) || !iszero(c.box.width.ch) if parent_abs_width == nothing || parent_abs_height == nothing c_abs_width = nothing else c_abs_width = parent_abs_width*(c.box.width.cw + cx_proportion(c.box.width,c.units)) + parent_abs_height*(c.box.width.ch + cy_proportion(c.box.width,c.units)) end end c_abs_height = c.box.height.abs if !iszero(c.box.height.cx) || !iszero(c.box.height.cy) || !iszero(c.box.height.cw) || !iszero(c.box.height.ch) if parent_abs_width == nothing || parent_abs_height == nothing c_abs_height = nothing else c_abs_height = parent_abs_width*(c.box.height.cw + cx_proportion(c.box.height,c.units)) + parent_abs_height*(c.box.height.ch + cy_proportion(c.box.height,c.units)) end end bb = BoundingBox(Measure(),Measure(),Measure(),Measure()) for child in c.container_children if !isa(child, Context) error("Can not compute boundingbox for graphics with non-Context containers") end cbb = boundingbox(child, linewidth, font, fontsize, c_abs_width, c_abs_height) width′ = transformcoordinates(cbb.width,child) height′ = transformcoordinates(cbb.height,child) x0′ = transformcoordinates(cbb.x0,child) y0′ = transformcoordinates(cbb.y0,child) bb′ = BoundingBox(child.box.x0+x0′, child.box.y0+y0′, width′, height′) bb = union(bb, bb′, c.units, c_abs_width, c_abs_height) end for child in c.form_children for prim in child.primitives newbb = boundingbox(prim, linewidth, font, fontsize) nextbb = union(bb, newbb, c.units, c_abs_height, c_abs_height) bb = nextbb end end return bb end # Frequently we can't compute the contents of a container without knowing its # absolute size, or it is one many possible layout that we want to decide # between before rendering. A ContainerPromise lets us defer computing a subtree # until the graphic is actually being rendered. abstract type ContainerPromise <: Container end # This information is passed to a container promise at drawtime. struct ParentDrawContext t::Transform units::UnitBox box::Absolute2DBox end # TODO: # Optionl in layouts will typically be expressed with ad hoc container promises, # since we can that way avoid realizing layout possibilities that are not used. # That means we need to be able to express size constraints on these. mutable struct AdhocContainerPromise <: ContainerPromise # A function of the form: # f(parent::ParentDrawContext) → Container f::Function # Z-order of this context relative to its siblings. order::Int # Ignore this context and everything under it if we are # not drawing to the SVGJS backend. withjs::Bool # Ignore this context if we are drawing on SVGJS withoutjs::Bool # Minimum sizes needed to draw the realized subtree correctly. minwidth::Maybe(Float64) minheight::Maybe(Float64) end function ctxpromise(f::Function; order=0, withjs::Bool=false, withoutjs::Bool=false, minwidth=nothing, minheight=nothing) if isa(minwidth, Measure) minwidth = minwidth.abs end if isa(minheight, Measure) minheight = minwidth.abs end return AdhocContainerPromise(f, order, withjs, withoutjs, minwidth, minheight) end realize(promise::AdhocContainerPromise, drawctx::ParentDrawContext) = promise.f(drawctx) function compose!(a::Context, b::Container) a.container_children = cons(b, a.container_children) return a end function compose!(a::Context, b::Form) a.form_children = cons(b, a.form_children) return a end function compose!(a::Context, b::Property) a.property_children = cons(b, a.property_children) return a end function compose(a::Context, b::ComposeNode) a = copy(a) compose!(a, b) return a end # higher-order compositions """ compose!(a::Context, b, c, ds...) -> a Add `b`, `c`, and `ds` to the graphic as children of `a`. """ compose!(a::Context, b, c, ds...) = compose!(compose!(a, b), c, ds...) compose!(a::Context, bs::AbstractArray) = compose!(a, compose!(bs...)) compose!(a::Context, bs::Tuple) = compose!(a, compose!(bs...)) compose!(a::Context) = a """ compose(a::Context, b, c, ds...) -> Context Add `b`, `c`, and `ds` to the graphic as children of a copy of `a`. """ compose(a::Context, b, c, ds...) = compose(compose(a, b), c, ds...) compose(a::Context, bs::AbstractArray) = compose(a, compose(bs...)) compose(a::Context, bs::Tuple) = compose(a, compose(bs...)) compose(a::Context) = a compose(a, b::Nothing) = a for (f, S, T) in [(:compose!, Property, Nothing), (:compose!, Form, Nothing), (:compose!, Property, Any), (:compose!, Form, Any), (:compose, Property, Nothing), (:compose, Form, Nothing), (:compose, Property, Any), (:compose, Form, Any)] eval( quote function $(f)(a::$(S), b::$(T)) error(@sprintf("Invalid composition: %s cannot be a root node.", $(S).name)) end end) end """ draw(b::Backend, c::Context) Output the tree-structured graphic in `c` to the given backend. # Examples ``` draw(SVGJS("foo.svg"), compose(context(), text(0,0,"foo"))) ``` """ function draw(backend::Backend, root_container::Container) isfinished(backend) && error("The backend has already been drawn upon.") drawpart(backend, root_container, IdentityTransform(), UnitBox(), root_box(backend)) finish(backend) end register_coords(backend::Backend, box, units, transform, form) = nothing struct DrawState pop_poperty::Bool container::Container parent_transform::Transform units::UnitBox parent_box::Absolute2DBox end DrawState(container, parent_transform, units, parent_box) = DrawState(false, container, parent_transform, units, parent_box) DrawState() = DrawState(true) ### ??? # Draw without finishing the backend # # Drawing is basically a depth-first traversal of the tree, pushing and popping # properties, expanding context promises, etc. as needed. # function drawpart(backend::Backend, container::Container, parent_transform::Transform, units::UnitBox, parent_box::Absolute2DBox) ((iswithjs(container) && !iswithjs(backend)) || (iswithoutjs(container) && iswithjs(backend))) && return if isa(container, ContainerPromise) container = realize(container, ParentDrawContext(parent_transform, units, parent_box)) isa(container, Container) || error("Error: A container promise function did not evaluate to a container") drawpart(backend, container, parent_transform, units, parent_box) return end ctx = optimize_batching(container) box = resolve(parent_box, units, parent_transform, ctx.box) transform = parent_transform if ctx.units !== nothing units = resolve(box, units, transform, ctx.units) end if ctx.rot !== nothing rot = resolve(box, units, parent_transform, ctx.rot) transform = combine(convert(Transform, rot), transform) end if ctx.mir !== nothing mir = resolve(box, units, parent_transform, ctx.mir) transform = combine(convert(Transform, mir), transform) end if ctx.shear !== nothing shear = resolve(box, units, parent_transform, ctx.shear) transform = combine(convert(Transform, shear), transform) end if ctx.raster && @isdefined(Cairo) && isa(backend, SVG) bitmapbackend = PNG(box.a[1], box.a[2], false) draw(bitmapbackend, ctx) f = bitmap("image/png", take!(bitmapbackend.out), 0, 0, 1w, 1h) c = context(ctx.box.x0[1], ctx.box.x0[2], ctx.box.a[1], ctx.box.a[2], units=UnitBox(), order=ctx.order, clip=ctx.clip) drawpart(backend, compose(c, f), parent_transform, units, parent_box) return end has_properties = false if !isa(ctx.property_children, ListNull) || ctx.clip has_properties = true properties = Array{Property}(undef, 0) child = ctx.property_children while !isa(child, ListNull) register_coords(backend, box, units, transform, child.head) push!(properties, resolve(parent_box, units, parent_transform, child.head)) child = child.tail end if ctx.clip x0 = ctx.box.x0[1] y0 = ctx.box.x0[2] x1 = x0 + ctx.box.a[1] y1 = y0 + ctx.box.a[2] push!(properties, resolve(parent_box, units, parent_transform, clip([(x0, y0), (x1, y0), (x1, y1), (x0, y1)]))) end push_property_frame(backend, properties) end trybatch = canbatch(backend) child = ctx.form_children while !isa(child, ListNull) register_coords(backend, box, units, transform, child.head) form = resolve(box, units, transform, child.head) if isempty(form.primitives) child = child.tail continue end if trybatch b = batch(form) if b !== nothing draw(backend, b) child = child.tail continue end end draw(backend, form) child = child.tail end # Order children if needed ordered_children = false child = ctx.container_children while !isa(child, ListNull) if order(child.head) != 0 ordered_children = true break end child = child.tail end if ordered_children container_children = Array{Tuple{Int, Int, Container}}(undef, 0) child = ctx.container_children while !isa(child, ListNull) push!(container_children, (order(child.head), 1 + length(container_children), child.head)) child = child.tail end sort!(container_children) for i in 1:length(container_children) drawpart(backend, container_children[i][3], transform, units, box) end empty!(container_children) else child = ctx.container_children while !isa(child, ListNull) drawpart(backend, child.head, transform, units, box) child = child.tail end end has_properties && pop_property_frame(backend) end # Produce a tree diagram representing the tree structure of a graphic. # # Args: # root: graphic to represent # # Returns: # A Context giving a tree diagram. # function introspect(root::Context) positions = Dict{ComposeNode, Tuple{Float64, Float64}}() level_count = Int[] max_level = 0 # TODO: It would be nice if we can try to do a better job of positioning # nodes within their levels q = Queue{Tuple{ComposeNode, Int}}() enqueue!(q, (root, 1)) figs = compose!(context(), stroke("#333"), linewidth(0.5mm)) figsize = 6mm while !isempty(q) node, level = dequeue!(q) if level > length(level_count) push!(level_count, 1) else level_count[level] += 1 end max_level = max(max_level, level) # draw shit fig = context(level_count[level] - 1, level - 1) if isa(node, Context) compose!(fig, circle(0.5, 0.5, figsize/2), fill(LCHab(92, 10, 77))) for child in children(node) enqueue!(q, (child, level + 1)) end elseif isa(node, Container) # TODO: should be slightly different than Context... compose!(fig, circle(0.5, 0.5, figsize/2), fill(LCHab(92, 10, 77))) elseif isa(node, Form) compose!(fig, (context(), text(0.5cx, 0.5cy, form_string(node), hcenter, vcenter), fill(colorant"black")), (context(), rectangle(0.5cx - figsize/2, 0.5cy - figsize/2, figsize, figsize), fill(LCHab(68, 74, 192)))) elseif isa(node, Property) # TODO: what should the third color be? compose!(fig, polygon([(0.5cx - figsize/2, 0.5cy - figsize/2), (0.5cx + figsize/2, 0.5cy - figsize/2), (0.5cx, 0.5cy + figsize/2)]), fill(LCHab(68, 74, 29))) else error("Unknown node type $(typeof(node))") end compose!(figs, fig) positions[node] = (level_count[level] - 0.5, level - 0.5) end # make a second traversal of the tree to draw lines between parents and # children lines_ctx = compose!(context(order=-1), stroke(LCHab(92, 10, 77))) enqueue!(q, (root, 1)) while !isempty(q) node, level = dequeue!(q) isa(node, Context) || continue pos = positions[node] for child in children(node) childpos = positions[child] compose!(lines_ctx, line([(pos[1], pos[2]), (childpos[1], childpos[2])])) enqueue!(q, (child, level + 1)) end end return compose!(context(units=UnitBox(0, 0, maximum(level_count), max_level)), (context(order=-2), rectangle(), fill("#333")), lines_ctx, figs) end function show(io::IO, ctx::Context) if get(io, :compact, false) print(io, "Context(") first = true for c in children(ctx) first || print(io, ",") first = false isa(c, AbstractArray) ? showcompact_array(io, c) : show(io, c) end print(io, ")") else invoke(show, Tuple{IO, Any}, io, ctx) end end function showcompact_array(io::IO, a::AbstractArray) print(io, "[") first = true for c in a first || print(io, ",") first = false show(io, c) end print(io, "]") end showcompact_array(io::IO, a::AbstractRange) = show(io, a) show(io::IO, f::Compose.Form) = get(io, :compact, false) ? print(io, Compose.form_string(f)) : invoke(show, Tuple{IO, Any}, io, f) show(io::IO, p::Compose.Property) = get(io, :compact, false) ? print(io, Compose.prop_string(p)) : invoke(show, Tuple{IO, Any}, io, p) show(io::IO, cp::ContainerPromise) = get(io, :compact, false) ? print(io, typeof(cp).name.name) : invoke(show, Tuple{IO, Any}, io, cp) ================================================ FILE: src/fontfallback.jl ================================================ # Font handling when pango and fontconfig are not available. # Define this even if we're not calling pango, since cairo needs it. const PANGO_SCALE = 1024.0 # Serialized glyph sizes for commont fonts. const glyphsizes = open(fd -> JSON.parse(read(fd, String)), joinpath(@__DIR__, "..", "deps", "glyphsize.json")) # It's better to overestimate text extents than to underestimes, since the later # leads to overlaping where the former just results in some extra space. So, we # scale estimated text extends by this number. Width on the other hand tends be # overestimated since it doesn't take kerning into account. const text_extents_scale_x = 1.0 const text_extents_scale_y = 1.0 # Normalized Levenshtein distance between two strings. function levenshtein(a::AbstractString, b::AbstractString) a = replace(lowercase(a), r"\s+"=>"") b = replace(lowercase(b), r"\s+"=>"") n = length(a) m = length(b) D = zeros(UInt, n + 1, m + 1) D[:,1] = 0:n D[1,:] = 0:m for i in 2:n, j in 2:m if a[i - 1] == b[j - 1] D[i, j] = D[i - 1, j - 1] else D[i, j] = 1 + min(D[i - 1, j - 1], D[i, j - 1], D[i - 1, j]) end end return D[n,m] / max(n, m) end # Find the nearst typeface from the glyph size table. let matched_font_cache = Dict{AbstractString, AbstractString}() global match_font function match_font(families::AbstractString) haskey(matched_font_cache, families) && return matched_font_cache[families] smallest_dist = Inf best_match = "Helvetica" for family in [lowercase(strip(family, [' ', '"', '\''])) for family in split(families, ',')] for available_family in keys(glyphsizes) d = levenshtein(family, available_family) if d < smallest_dist smallest_dist = d best_match = available_family end end end matched_font_cache[families] = best_match return best_match end end # Approximate width of a text in millimeters. # # Args: # widths: A glyph width table from glyphsizes. # text: Any string. # size: Font size in points. # # Returns: # Approximate text width in millimeters. # function text_width(widths::Dict, text::AbstractString, size::Float64) stripped_text = replace(text, r"<[^>]*>"=>"") width = 0 for c in stripped_text width += get(widths, string(c), widths["w"]) end width end function max_text_extents(font_family::AbstractString, size::Measure, texts::AbstractString...) isa(size, AbsoluteLength) || error("text_extents requries font size be in absolute units") scale = size / 12pt font_family = match_font(font_family) glyphheight = glyphsizes[font_family]["height"] widths = glyphsizes[font_family]["widths"] fontsize = size/pt chunkwidths = Float64[] textheights = Float64[] for text in texts textheight = 0.0 for chunk in split(text, '\n') chunkheight = glyphheight if match(r"", String(chunk)) != nothing chunkheight *= 1.5 end textheight += chunkheight push!(chunkwidths, text_width(widths, chunk, fontsize)) end push!(textheights, textheight) end width = maximum(chunkwidths) height = maximum(textheights) (text_extents_scale_x * scale * width * mm, text_extents_scale_y * scale * height * mm) end function text_extents(font_family::AbstractString, size::Measure, texts::AbstractString...) scale = size / 12pt font_family = match_font(font_family) glyphheight = glyphsizes[font_family]["height"] glyphwidths = glyphsizes[font_family]["widths"] fontsize = size/pt extents = Array{Tuple{Measure, Measure}}(undef, length(texts)) for (i, text) in enumerate(texts) chunkwidths = Float64[] textheight = 0.0 for chunk in split(text, "\n") chunkheight = glyphheight if match(r"", String(chunk)) != nothing chunkheight *= 1.5 end textheight += chunkheight push!(chunkwidths, text_width(glyphwidths, chunk, fontsize)) end width = maximum(chunkwidths) extents[i] = (text_extents_scale_x * scale * width * mm, text_extents_scale_y * scale * textheight * mm) end return extents end const carriage_shift0 = 1.1 const carriage_shift_supsub = 0.1 const supsub_shift = 0.4 # Amazingly crude fallback to parse pango markup into svg. function pango_to_svg(text::AbstractString) pat = r"<(/?)\s*([^>]*)\s*>" output = IOBuffer() output_line = IOBuffer() textlines = split(text, "\n") carriage_shift = carriage_shift0 for (itextline,textline) in enumerate(textlines) input = codeunits(textline) lastpos = 1 baseline_shift = 0.0 sup = sub = false open_tag = false for mat in eachmatch(pat, String(textline)) write(output_line, input[lastpos:mat.offset-1]) lastpos = mat.offset + length(mat.match) closing_tag = mat.captures[1] == "/" open_tag && !closing_tag && write(output_line, "") if closing_tag write(output_line, "") else if mat.captures[2] == "sup" write(output_line, "") baseline_shift = -supsub_shift * 0.83 sup = true elseif mat.captures[2] == "sub" write(output_line, "") baseline_shift = supsub_shift * 0.83 sub = true elseif mat.captures[2] == "i" write(output_line, "") elseif mat.captures[2] == "b" write(output_line, "") end end if closing_tag && baseline_shift != 0.0 if lastpos < length(input) @printf(output_line, "", -baseline_shift) baseline_shift = 0.0 open_tag = true else open_tag = false end end end write(output_line, input[lastpos:end]) open_tag && write(output_line, "") itextline>1 && @printf(output, "", carriage_shift + sup*carriage_shift_supsub) write(output, String(take!(output_line))) itextline>1 && write(output, "") carriage_shift = carriage_shift0 - baseline_shift + sub*carriage_shift_supsub end String(take!(output)) end ================================================ FILE: src/form.jl ================================================ # A form is something that ends up as geometry in the graphic. abstract type FormPrimitive end const empty_tag = Symbol("") struct Form{P <: FormPrimitive} <: ComposeNode primitives::Vector{P} tag::Symbol Form{P}(prim, tag::Symbol=empty_tag) where P = new{P}(prim, tag) end Form(primitives::Vector{P}, tag::Symbol=empty_tag) where P <: FormPrimitive = Form{P}(primitives, tag) isempty(f::Form) = isempty(f.primitives) isscalar(f::Form) = length(f.primitives) == 1 resolve(box::AbsoluteBox, units::UnitBox, t::Transform, form::Form) = Form([resolve(box, units, t, primitive) for primitive in form.primitives]) Base.similar(f::Form{T}) where T = Form{T}(T[]) form_string(::Form) = "FORM" # fallback definition # Polygon # ------- struct SimplePolygonPrimitive{P <: Vec} <: FormPrimitive points::Vector{P} end const SimplePolygon{P<:SimplePolygonPrimitive} = Form{P} const Polygon = SimplePolygon const PolygonPrimitive = SimplePolygonPrimitive polygon() = Form([PolygonPrimitive(Vec[])]) """ polygon(points) Define a polygon. `points` is an array of `(x,y)` tuples that specify the corners of the polygon. """ function polygon(points::AbstractArray{T}, tag=empty_tag) where T <: XYTupleOrVec XM, YM = narrow_polygon_point_types(Vector[points]) if XM == Any XM = Measure end if YM == Any YM = Measure end VecType = Tuple{XM, YM} return Form([PolygonPrimitive(VecType[(x_measure(point[1]), y_measure(point[2])) for point in points])], tag) end """ polygon(point_arrays::AbstractArray) Arguments can be passed in arrays in order to perform multiple drawing operations at once. """ function polygon(point_arrays::AbstractArray, tag=empty_tag) XM, YM = narrow_polygon_point_types(point_arrays) VecType = XM == YM == Any ? Vec : Tuple{XM, YM} PrimType = XM == YM == Any ? PolygonPrimitive : PolygonPrimitive{VecType} polyprims = Array{PrimType}(undef, length(point_arrays)) for (i, point_array) in enumerate(point_arrays) polyprims[i] = PrimType(VecType[(x_measure(point[1]), y_measure(point[2])) for point in point_array]) end return Form{PrimType}(polyprims, tag) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::PolygonPrimitive) = PolygonPrimitive{AbsoluteVec2}( AbsoluteVec2[ resolve(box, units, t, point) for point in p.points]) function boundingbox(form::PolygonPrimitive, linewidth::Measure, font::AbstractString, fontsize::Measure) x0 = minimum([p[1] for p in form.points]) x1 = maximum([p[1] for p in form.points]) y0 = minimum([p[2] for p in form.points]) y1 = maximum([p[2] for p in form.points]) return BoundingBox(x0 - linewidth, y0 - linewidth, x1 - x0 + linewidth, y1 - y0 + linewidth) end form_string(::SimplePolygon) = "SP" struct ComplexPolygonPrimitive{P <: Vec} <: FormPrimitive rings::Vector{Vector{P}} end const ComplexPolygon{P<:ComplexPolygonPrimitive} = Form{P} complexpolygon() = ComplexPolygon([ComplexPolygonPrimitive(Vec[])]) function complexpolygon(rings::Vector{Vector}, tag=empty_tag) XM, YM = narrow_polygon_point_types(rings) if XM == Any XM = Length{:cx, Float64} end if YM == Any YM = Length{:cy, Float64} end VecType = Tuple{XM, YM} return ComplexPolygon([ ComplexPolygonPrimitive([VecType[(x_measure(point[1]), y_measure(point[2])) for point in points]])], tag) end function complexpolygon(ring_arrays::Vector{Vector{Vector}}, tag=empty_tag) XM, YM = narrow_polygon_point_types(coords) VecType = XM == YM == Any ? Vec : Tuple{XM, YM} PrimType = XM == YM == Any ? ComplexPolygonPrimitive : ComplexPolygonPrimitive{VecType} ComplexPolygon([PrimType[[(x_measure(x), y_measure(y)) for (x, y) in ring] for ring in ring_array] for ring_array in ring_arrays], tag) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::ComplexPolygonPrimitive) = ComplexPolygonPrimitive( [AbsoluteVec2[ resolve(box, units, t, point, t) for point in ring] for ring in p.rings]) form_string(::ComplexPolygon) = "CP" # Rectangle # --------- struct RectanglePrimitive{P <: Vec, M1 <: Measure, M2 <: Measure} <: FormPrimitive corner::P width::M1 height::M2 end const Rectangle{P<:RectanglePrimitive} = Form{P} """ rectangle() Define a rectangle that fills the current context completely. """ function rectangle() prim = RectanglePrimitive((0.0w, 0.0h), 1.0w, 1.0h) return Rectangle{typeof(prim)}([prim]) end """ rectangle(x0, y0, width, height) Define a rectangle of size `width`x`height` with its top left corner at the point (`x`, `y`). """ function rectangle(x0, y0, width, height, tag=empty_tag) corner = (x_measure(x0), y_measure(y0)) width = x_measure(width) height = y_measure(height) prim = RectanglePrimitive(corner, width, height) return Rectangle{typeof(prim)}([prim], tag) end """ rectangle(x0s::AbstractArray, y0s::AbstractArray, widths::AbstractArray, heights::AbstractArray) Arguments can be passed in arrays in order to perform multiple drawing operations at once. """ rectangle(x0s::AbstractArray, y0s::AbstractArray, widths::AbstractArray, heights::AbstractArray, tag=empty_tag) = @makeform (x0 in x0s, y0 in y0s, width in widths, height in heights), RectanglePrimitive{Vec2, Measure, Measure}((x_measure(x0), y_measure(y0)), x_measure(width), y_measure(height)) tag function resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::RectanglePrimitive) corner = resolve(box, units, t, p.corner) width = resolve(box, units, t, p.width) height = resolve(box, units, t, p.height) if isxflipped(units) && hasunits(Length{:cx}, p.corner[1]) # if coordinates are flipped we end up with the corner on the other end # of the rectangle, which is fix here x = corner[1] - width else x = corner[1] end if isyflipped(units) && hasunits(Length{:cy}, p.corner[2]) y = corner[2] - height else y = corner[2] end return RectanglePrimitive{AbsoluteVec2, AbsoluteLength, AbsoluteLength}( (x, y), width, height) end boundingbox(form::RectanglePrimitive, linewidth::Measure, font::AbstractString, fontsize::Measure) = BoundingBox(form.corner.x - linewidth, form.corner.y - linewidth, form.width + 2*linewidth, form.height + 2*linewidth) form_string(::Rectangle) = "R" # Circle # ------ struct CirclePrimitive{P <: Vec, M <: Measure} <: FormPrimitive center::P radius::M end CirclePrimitive(center::P, radius::M) where {P, M} = CirclePrimitive{P, M}(center, radius) CirclePrimitive(x, y, r) = CirclePrimitive((x_measure(x), y_measure(y)), x_measure(r)) const Circle{P<:CirclePrimitive} = Form{P} """ circle() Define a circle in the center of the current context with a diameter equal to the width of the context. """ function circle() prim = CirclePrimitive((0.5w, 0.5h), 0.5w) return Circle{typeof(prim)}([prim]) end """ circle(x, y, r) Define a circle with its center at (`x`,`y`) and a radius of `r`. """ function circle(x, y, r, tag=empty_tag) prim = CirclePrimitive(x, y, r) return Circle{typeof(prim)}([prim], tag) end """ circle(xs::AbstractArray, ys::AbstractArray, rs::AbstractArray) Arguments can be passed in arrays in order to perform multiple drawing operations. """ function circle(xs::AbstractArray, ys::AbstractArray, rs::AbstractArray, tag=empty_tag) if isempty(xs) || isempty(ys) || isempty(rs) prima = CirclePrimitive[] return Circle{eltype(prima)}(prima, tag) end return @makeform (x in xs, y in ys, r in rs), CirclePrimitive{Vec2, Measure}((x_measure(x), y_measure(y)), x_measure(r)) tag end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::CirclePrimitive) = CirclePrimitive{AbsoluteVec2, AbsoluteLength}( resolve(box, units, t, p.center), resolve(box, units, t, p.radius)) boundingbox(form::CirclePrimitive, linewidth::Measure, font::AbstractString, fontsize::Measure) = BoundingBox(form.center[1] - form.radius - linewidth, form.center[2] - form.radius - linewidth, 2 * (form.radius + linewidth), 2 * (form.radius + linewidth)) form_string(::Circle) = "C" # Ellipse # ------- struct EllipsePrimitive{P1<:Vec, P2<:Vec, P3<:Vec} <: FormPrimitive center::P1 x_point::P2 y_point::P3 end const Ellipse{P<:EllipsePrimitive} = Form{P} """ ellipse() Define an ellipse in the center of the current context with `x_radius=0.5w` and `y_radius=0.5h`. """ function ellipse() prim = EllipsePrimitive((0.5w, 0.5h), (1.0w, 0.5h), (0.5w, 1.0h)) return Ellipse{typeof(prim)}([prim]) end """ ellipse(x, y, x_radius, y_radius) Define an ellipse with its center at (`x`,`y`) with radii `x_radius` and `y_radius`. """ function ellipse(x, y, x_radius, y_radius, tag=empty_tag) xm = x_measure(x) ym = y_measure(y) prim = EllipsePrimitive((xm, ym), (xm + x_measure(x_radius), ym), (xm, ym + y_measure(y_radius))) return Ellipse{typeof(prim)}([prim], tag) end """ ellipse(xs::AbstractArray, ys::AbstractArray, x_radiuses::AbstractArray, y_radiuses::AbstractArray) Arguments can be passed in arrays in order to perform multiple drawing operations. """ function ellipse(xs::AbstractArray, ys::AbstractArray, x_radiuses::AbstractArray, y_radiuses::AbstractArray, tag=empty_tag) return @makeform (x in xs, y in ys, x_radius in x_radiuses, y_radius in y_radiuses), EllipsePrimitive((x_measure(x), y_measure(y)), (x_measure(x) + x_measure(x_radius), y_measure(y)), (x_measure(x), y_measure(y) + y_measure(y_radius))) tag end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::EllipsePrimitive) = EllipsePrimitive{AbsoluteVec2, AbsoluteVec2, AbsoluteVec2}( resolve(box, units, t, p.center), resolve(box, units, t, p.x_point), resolve(box, units, t, p.y_point)) function boundingbox(form::EllipsePrimitive, linewidth::Measure, font::AbstractString, fontsize::Measure) x0 = min(form.x_point.x, form.y_point.x) x1 = max(form.x_point.x, form.y_point.x) y0 = min(form.x_point.y, form.y_point.y) y1 = max(form.x_point.y, form.y_point.y) xr = x1 - x0 yr = y1 - y0 return BoundingBox(x0 - linewidth - xr, y0 - linewidth - yr, 2 * (xr + linewidth), 2 * (yr + linewidth)) end form_string(::Ellipse) = "E" # Text # ---- abstract type HAlignment end struct HLeft <: HAlignment end struct HCenter <: HAlignment end struct HRight <: HAlignment end const hleft = HLeft() const hcenter = HCenter() const hright = HRight() abstract type VAlignment end struct VTop <: VAlignment end struct VCenter <: VAlignment end struct VBottom <: VAlignment end const vtop = VTop() const vcenter = VCenter() const vbottom = VBottom() struct TextPrimitive{P<:Vec, R<:Rotation, O<:Vec} <: FormPrimitive position::P value::AbstractString halign::HAlignment valign::VAlignment # Text forms need their own rotation field unfortunately, since there is no # way to give orientation with just a position point. rot::R offset::O end const Text{P<:TextPrimitive} = Form{P} """ text(x, y, value [,halign::HAlignment [,valign::VAlignment [,rot::Rotation]]]) Draw the text `value` at the position (`x`,`y`) relative to the current context. The default alignment of the text is `hleft` `vbottom`. The vertical and horizontal alignment is specified by passing `hleft`, `hcenter` or `hright` and `vtop`, `vcenter` or `vbottom` as values for `halign` and `valign` respectively. """ function text(x, y, value, halign::HAlignment=hleft, valign::VAlignment=vbottom, rot=Rotation(), offset::Vec2=(0mm,0mm); tag::Symbol=empty_tag) moffset = (x_measure(offset[1]), y_measure(offset[2])) prim = TextPrimitive((x_measure(x), y_measure(y)), string(value), halign, valign, rot, moffset) Text{typeof(prim)}([prim], tag) end """ text(xs::AbstractArray, ys::AbstractArray, values::AbstractArray [,haligns::HAlignment [,valigns::VAlignment [,rots::Rotation]]]) Arguments can be passed in arrays in order to perform multiple drawing operations at once. """ text(xs::AbstractArray, ys::AbstractArray, values::AbstractArray{AbstractString}, haligns::AbstractArray=[hleft], valigns::AbstractArray=[vbottom], rots::AbstractArray=[Rotation()], offsets::AbstractArray=[(0mm,0mm)]; tag::Symbol=empty_tag) = @makeform (x in xs, y in ys, value in values, halign in haligns, valign in valigns, rot in rots, offset in offsets), TextPrimitive((x_measure(x), y_measure(y)), value, halign, valign, rot, (x_measure(offset[1]), y_measure(offset[2]))) tag text(xs::AbstractArray, ys::AbstractArray, values::AbstractArray, haligns::AbstractArray=[hleft], valigns::AbstractArray=[vbottom], rots::AbstractArray=[Rotation()], offsets::AbstractArray=[(0mm,0mm)]; tag::Symbol=empty_tag) = @makeform (x in xs, y in ys, value in values, halign in haligns, valign in valigns, rot in rots, offset in offsets), TextPrimitive((x_measure(x), y_measure(y)), value, halign, valign, rot, (x_measure(offset[1]), y_measure(offset[2]))) tag function resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::TextPrimitive{P,R,O}) where {P,R,O} rot = resolve(box, units, t, p.rot) return TextPrimitive{AbsoluteVec2, typeof(rot), O}( resolve(box, units, t, p.position), p.value, p.halign, p.valign, rot, p.offset) end function boundingbox(form::TextPrimitive, linewidth::Measure, font::AbstractString, fontsize::Measure) width, height = text_extents(font, fontsize, form.value)[1] if form.halign == hleft x0 = form.position.x elseif form.halign == hcenter x0 = form.position.x - width/2 elseif form.halign == hright x0 = form.position.x - width end if form.valign == vbottom y0 = form.position.y - height elseif form.valign == vcenter y0 = form.position.y - height/2 elseif form.valign == vtop y0 = form.position.y end return BoundingBox(x0 - linewidth + form.offset[1], y0 - linewidth + form.offset[2], width + linewidth, height + linewidth) end form_string(::Text) = "T" # Line # ---- struct LinePrimitive{P<:Vec} <: FormPrimitive points::Vector{P} end const Line{P<:LinePrimitive} = Form{P} function line() prim = LinePrimitive(Vec[]) return Line{typeof(prim)}([prim]) end """ line(points) Define a line. `points` is an array of `(x,y)` tuples. """ function line(points::AbstractArray{T}, tag=empty_tag) where T <: XYTupleOrVec XM, YM = narrow_polygon_point_types(Vector[points]) VecType = XM == YM == Any ? Vec2 : Tuple{XM, YM} prim = LinePrimitive(VecType[(x_measure(point[1]), y_measure(point[2])) for point in points]) return Line{typeof(prim)}([prim], tag) end """ line(point_arrays::AbstractArray) Arguments can be passed in arrays in order to perform multiple drawing operations at once. """ function line(point_arrays::AbstractArray, tag=empty_tag) XM, YM = narrow_polygon_point_types(point_arrays) VecType = XM == YM == Any ? Vec2 : Tuple{XM, YM} PrimType = XM == YM == Any ? LinePrimitive : LinePrimitive{VecType} lineprims = Array{PrimType}(undef, length(point_arrays)) for (i, point_array) in enumerate(point_arrays) p = PrimType(VecType[(x_measure(point[1]), y_measure(point[2])) for point in point_array]) lineprims[i] = p end return Form{PrimType}(lineprims, tag) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::LinePrimitive) = LinePrimitive{AbsoluteVec2}( AbsoluteVec2[resolve(box, units, t, point) for point in p.points]) function boundingbox(form::LinePrimitive, linewidth::Measure, font::AbstractString, fontsize::Measure) x0 = minimum([p.x for p in form.points]) x1 = maximum([p.x for p in form.points]) y0 = minimum([p.y for p in form.points]) y1 = maximum([p.y for p in form.points]) return BoundingBox(x0 - linewidth, y0 - linewidth, x1 - x0 + linewidth, y1 - y0 + linewidth) end form_string(::Line) = "L" # Curve # ----- struct CurvePrimitive{P1<:Vec, P2<:Vec, P3<:Vec, P4<:Vec} <: FormPrimitive anchor0::P1 ctrl0::P2 ctrl1::P3 anchor1::P4 end const Curve{P<:CurvePrimitive} = Form{P} """ curve(anchor0, ctrl0, ctrl1, anchor1) Define a bezier curve between `anchor0` and `anchor1` with control points `ctrl0` and `ctrl1`. """ function curve(anchor0::XYTupleOrVec, ctrl0::XYTupleOrVec, ctrl1::XYTupleOrVec, anchor1::XYTupleOrVec, tag=empty_tag) prim = CurvePrimitive((x_measure(anchor0[1]), y_measure(anchor0[2])), (x_measure(ctrl0[1]), y_measure(ctrl0[2])), (x_measure(ctrl1[1]), y_measure(ctrl1[2])), (x_measure(anchor1[1]), y_measure(anchor1[2]))) return Curve{typeof(prim)}([prim], tag) end """ curve(anchor0s::AbstractArray, ctrl0s::AbstractArray, ctrl1s::AbstractArray, anchor1s::AbstractArray) Arguments can be passed in arrays in order to perform multiple drawing operations. """ curve(anchor0s::AbstractArray, ctrl0s::AbstractArray, ctrl1s::AbstractArray, anchor1s::AbstractArray, tag=empty_tag) = @makeform (anchor0 in anchor0s, ctrl0 in ctrl0s, ctrl1 in ctrl1s, anchor1 in anchor1s), CurvePrimitive((x_measure(anchor0[1]), y_measure(anchor0[2])), (x_measure(ctrl0[1]), y_measure(ctrl0[2])), (x_measure(ctrl1[1]), y_measure(ctrl1[2])), (x_measure(anchor1[1]), y_measure(anchor1[2]))) tag resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::CurvePrimitive) = CurvePrimitive{AbsoluteVec2, AbsoluteVec2, AbsoluteVec2, AbsoluteVec2}( resolve(box, units, t, p.anchor0), resolve(box, units, t, p.ctrl0), resolve(box, units, t, p.ctrl1), resolve(box, units, t, p.anchor1)) form_string(::Curve) = "CV" # Bitmap # ------ struct BitmapPrimitive{P <: Vec, XM <: Measure, YM <: Measure} <: FormPrimitive mime::AbstractString data::Vector{UInt8} corner::P width::XM height::YM end const Bitmap{P<:BitmapPrimitive} = Form{P} """ bitmap(mime, data, x0, y0, width, height) Define a bitmap of size `width`x`height` with its top left corner at the point (`x`, `y`). """ function bitmap(mime::AbstractString, data::Vector{UInt8}, x0, y0, width, height, tag=empty_tag) corner = (x_measure(x0), y_measure(y0)) width = x_measure(width) height = y_measure(height) prim = BitmapPrimitive(mime, data, corner, width, height) return Bitmap{typeof(prim)}([prim], tag) end """ bitmap(mimes::AbstractArray, datas::AbstractArray, x0s::AbstractArray, y0s::AbstractArray, widths::AbstractArray, heights::AbstractArray) Arguments can be passed in arrays in order to perform multiple drawing operations. """ bitmap(mimes::AbstractArray, datas::AbstractArray, x0s::AbstractArray, y0s::AbstractArray, widths::AbstractArray, heights::AbstractArray, tag=empty_tag) = @makeform (mime in mimes, data in datas, x0 in x0s, y0 in y0s, width in widths, height in heights), BitmapPrimitive(mime, data, (x_measure(x0), y_measure(y0)), x_measure(width), y_measure(height)) tag resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::BitmapPrimitive) = BitmapPrimitive{AbsoluteVec2, AbsoluteLength, AbsoluteLength}( p.mime, p.data, resolve(box, units, t, p.corner), resolve(box, units, t, p.width), resolve(box, units, t, p.height)) boundingbox(form::BitmapPrimitive, linewidth::Measure, font::AbstractString, fontsize::Measure) = BoundingBox(form.corner.x, form.corner.y, form.width, form.height) form_string(::Bitmap) = "B" # Arc # ------- struct ArcPrimitive{P<:Vec, M<:Measure} <: Compose.FormPrimitive center::P radius::M angle1::Float64 angle2::Float64 sector::Bool end ArcPrimitive(center::P, radius::M, angle1, angle2, sector) where {P, M} = ArcPrimitive{P, M}(center, radius, angle1, angle2, sector) ArcPrimitive(x, y, r, θ1, θ2, sector) = ArcPrimitive((x_measure(x), y_measure(y)), x_measure(r), θ1, θ2, sector) Arc{P<:ArcPrimitive} = Compose.Form{P} """ arc(x, y, r, θ1, θ2, sector) Define an arc with its center at (`x`,`y`), radius of `r`, between `θ1` and `θ2`. `sector` (optional) is true or false, true for a pie sector, false for an arc. Arcs are drawn clockwise from θ1 to θ2. """ function arc(x, y, r, θ1, θ2, sector=false, tag=empty_tag) prim = ArcPrimitive(x, y, r, θ1, θ2, sector) return Arc{typeof(prim)}([prim], tag) end """ sector(x, y, r, θ1, θ2) Define a pie sector with its center at (`x`,`y`), radius of `r`, between `θ1` and `θ2`. """ sector(x, y, r, θ1, θ2) = arc(x,y,r,θ1,θ2,true) """ arc(xs::AbstractVector, ys::AbstractVector, rs::AbstractVector, θ1s::AbstractVector, θ2s::AbstractVector, sectors::AbstractVector) Arguments can be passed in arrays in order to perform multiple drawing operations. """ function arc(xs::AbstractVector, ys::AbstractVector, rs::AbstractVector, θ1s::AbstractVector, θ2s::AbstractVector, sectors::AbstractVector=[false], tag=empty_tag) return @makeform (x in xs, y in ys, r in rs, θ1 in θ1s, θ2 in θ2s, sector in sectors), ArcPrimitive((x_measure(x), y_measure(y)), x_measure(r), θ1, θ2, sector) tag end """ sector(xs::AbstractVector, ys::AbstractVector, rs::AbstractVector, θ1s::AbstractVector, θ2s::AbstractVector) Arguments can be passed in arrays in order to perform multiple drawing operations. """ sector(xs::AbstractVector, ys::AbstractVector, rs::AbstractVector, θ1s::AbstractVector, θ2s::AbstractVector) = arc(xs, ys, rs, θ1s, θ2s, [true]) resolve(box::AbsoluteBox, units::UnitBox, t::Compose.Transform, p::ArcPrimitive) = ArcPrimitive{AbsoluteVec2, AbsoluteLength}( resolve(box, units, t, p.center), resolve(box, units, t, p.radius), p.angle1, p.angle2, p.sector) boundingbox(form::ArcPrimitive, linewidth::Measure, font::AbstractString, fontsize::Measure) = BoundingBox(form.center[1] - form.radius - linewidth, form.center[2] - form.radius - linewidth, 2 * (form.radius + linewidth), 2 * (form.radius + linewidth)) form_string(::Arc) = "A" @deprecate slice(x,y,r,θ1,θ2) sector(x,y,r,θ1,θ2) ### Polygon primitive forms """ ngon(x, y, r, n::Int) Define a `n`-sided polygon with its center at (`x`,`y`), and radius of `r`. For an upside-down ngon, use `-r`. """ function ngon(x, y, r, n::Int, tag=empty_tag) θ = range(-π/2, stop=1.5π, length=n+1) x1 = x_measure(x) .+ size_x_measure(r).*cos.(θ) y1 = y_measure(y) .+ size_x_measure(r).*sin.(θ) points = collect(Tuple{Measure, Measure}, zip(x1, y1)) return Form([PolygonPrimitive(points)], tag) end """ ngon(xs::AbstractVector, ys::AbstractVector, rs::AbstractVector, ns::AbstractVector{Int}) Arguments can be passed in arrays in order to perform multiple drawing operations at once. """ function ngon(xs::AbstractVector, ys::AbstractVector, rs::AbstractVector, ns::AbstractVector{Int}, tag=empty_tag) VecType = Tuple{Measure, Measure} PrimType = PolygonPrimitive{VecType} polyprims = PrimType[] for (x, y, r, n) in Compose.cyclezip(xs, ys, rs, ns) p = ngon( x, y, r, n) push!(polyprims, PrimType(p.primitives[1].points)) end return Form{PrimType}(polyprims, tag) end """ star(x, y, r, n::Int, ratio) Define a `n`-pointed star with its center at (`x`,`y`), outer radius of `r`, and inner radius equal to `r*ratio`. For an upside-down star, use `-r`. """ function star(x, y, r, n::Int, ratio::Float64=0.3, tag=empty_tag) θ = range(-π/2, stop=1.5π, length=2*n+1)[1:end-1] r1 = repeat([r, r*ratio], outer=n) x1 = x_measure(x) .+ size_x_measure(r1).*cos.(θ) y1 = y_measure(y) .+ size_x_measure(r1).*sin.(θ) points = collect(Tuple{Measure, Measure}, zip(x1, y1)) return Form([PolygonPrimitive(points)], tag) end """ star(xs::AbstractVector, ys::AbstractVector, rs::AbstractVector, ns::AbstractVector{Int}, ratios::AbstractVector{Float64}) Arguments can be passed in arrays in order to perform multiple drawing operations at once. """ function star(xs::AbstractVector, ys::AbstractVector, rs::AbstractVector, ns::AbstractVector{Int}, ratios::AbstractVector{Float64}=[0.3], tag=empty_tag) VecType = Tuple{Measure, Measure} PrimType = PolygonPrimitive{VecType} polyprims = PrimType[] for (x, y, r, n, ratio) in Compose.cyclezip(xs, ys, rs, ns, ratios) p = star( x, y, r, n, ratio) push!(polyprims, PrimType(p.primitives[1].points)) end return Form{PrimType}(polyprims, tag) end """ xgon(x, y, r, n::Int, ratio) Define a cross with `n` arms with its center at (`x`,`y`), outer radius of `r`, and inner radius equal to `r*ratio`. For an upside-down xgon, use `-r`. """ function xgon(x, y, r, n::Int, ratio::Float64=0.1, tag=empty_tag) θ₁ = range(-0.75π, stop=1.25π, length=n+1)[1:end-1] w = 2*r*ratio*sin(π/n) dₒ = abs(asin(0.5*w/r)) dᵢ = abs(asin(0.5*w/(r*ratio))) r₂ = repeat([r*ratio,r,r], outer=n) θ₂ = vec([θ+x for x in [-dᵢ, -dₒ, dₒ], θ in θ₁]) x1 = x_measure(x) .+ size_x_measure(r₂).*cos.(θ₂) y1 = y_measure(y) .+ size_x_measure(r₂).*sin.(θ₂) points = collect(Tuple{Measure, Measure}, zip(x1, y1)) return Form([PolygonPrimitive(points)], tag) end """ xgon(xs::AbstractVector, ys::AbstractVector, rs::AbstractVector, ns::AbstractVector{Int}, ratios::AbstractVector{Float64}) Arguments can be passed in arrays in order to perform multiple drawing operations at once. """ function xgon(xs::AbstractVector, ys::AbstractVector, rs::AbstractVector, ns::AbstractVector{Int}, ratios::AbstractVector{Float64}=[0.3], tag=empty_tag) VecType = Tuple{Measure, Measure} PrimType = PolygonPrimitive{VecType} polyprims = PrimType[] for (x, y, r, n, ratio) in Compose.cyclezip(xs, ys, rs, ns, ratios) p = xgon(x, y, r, n, ratio) push!(polyprims, PrimType(p.primitives[1].points)) end return Form{PrimType}(polyprims, tag) end """ points(x::Compose.Form) Extract points from a Compose.Form """ points(x::Compose.Form) = x.primitives[1].points # Bezigon # ------- struct BezierPolygonPrimitive{P<:Vec} <: FormPrimitive anchor::P sides::Vector{Vector{P}} end Bezigon{P<:BezierPolygonPrimitive} = Form{P} """ bezigon(anchor0::Tuple, sides::Vector{<:Vector{<:Tuple}}) Define a bezier polygon. `anchor0` is the starting point as an `(x,y)` tuple. `sides` contains Vectors of side points (tuples): each vector has the control point(s) and end point for each side (the end point forms the next starting point). The sides can be linear (1 point), quadratic (2 points) or cubic (3 points). """ function bezigon(anchor0::XYTupleOrVec, sides::Vector{T}, tag=empty_tag) where T<:Vector{<:XYTupleOrVec} anchor = (x_measure(anchor0[1]), y_measure(anchor0[2])) sv = Vector{Vec2}[] for side in sides s = collect(Vec2, zip(x_measure.(first.(side)), y_measure.(last.(side)))) push!(sv, s) end Form([BezierPolygonPrimitive(anchor, sv)], tag) end """ bezigon(anchors::Vector{Tuple}, polysides=Vector{<:Vector{<:Vector{<:Tuple}}}) Arguments can be passed in arrays in order to perform multiple drawing operations at once. """ function bezigon(anchors::Vector, polysides::Vector{T}, tag=empty_tag) where T<:Vector{<:Vector} polyprims = BezierPolygonPrimitive[] for (anchor0, sides) in cyclezip(anchors, polysides) anchor = (x_measure(anchor0[1]), y_measure(anchor0[2])) sv = Vector{Vec2}[] for side in sides s = collect(Vec2, zip(x_measure.(first.(side)), y_measure.(last.(side)))) push!(sv, s) end push!(polyprims, BezierPolygonPrimitive(anchor, sv)) end Form(polyprims, tag) end function resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::BezierPolygonPrimitive) anchor = resolve(box, units, t, p.anchor) sv = Vector{Vec}[] for side in p.sides push!(sv, [resolve(box, units, t, point) for point in side]) end return BezierPolygonPrimitive(anchor, sv) end function boundingbox(prim::BezierPolygonPrimitive, linewidth::Measure, font::AbstractString, fontsize::Measure) points = [prim.anchor; reduce(vcat, prim.sides)] x, y = first.(points), last.(points) x0, x1 = extrema(x) y0, y1 = extrema(y) return BoundingBox(x0-linewidth, y0-linewidth, x1-x0+linewidth, y1-y0+linewidth) end form_string(::Bezigon) = "BP" ================================================ FILE: src/immerse_backend.jl ================================================ # The Immerse backend # To the Cairo backend, this adds just one feature: keeping track of # the rendered coordinates of tagged objects and svgclass("plotpanel") # objects export ImmerseBackend mutable struct ImmerseBackend <: Backend cb::CAIROSURFACE coords::Dict{Symbol,Any} panelcoords::Vector end function ImmerseBackend(c::CairoSurface) cb = CAIROSURFACE(c) ImmerseBackend(cb, Dict{Symbol,Any}(), Any[]) end function (img::ImmerseBackend)(x) draw(img, x) end root_box(backend::ImmerseBackend) = root_box(backend.cb) push_property_frame(backend::ImmerseBackend, properties) = push_property_frame(backend.cb, properties) pop_property_frame(backend::ImmerseBackend) = pop_property_frame(backend.cb) function draw(backend::ImmerseBackend, root_container::Container) empty!(backend.coords) empty!(backend.panelcoords) drawpart(backend, root_container, IdentityTransform(), UnitBox(), root_box(backend)) finish(backend) end function register_coords(backend::ImmerseBackend, box, units, transform, form::Form) if form.tag != empty_tag backend.coords[form.tag] = (box, units, transform) end nothing end function register_coords(backend::ImmerseBackend, box, units, transform, property::SVGClass) length(property.primitives) == 1 && property.primitives[1].value == "plotpanel" && push!(backend.panelcoords, (box, units, transform)) nothing end absolute_native_units(backend::ImmerseBackend, u::Float64) = absolute_native_units(backend.cb, u) absolute_native_units(backend::ImmerseBackend, l::Length{:mm}) = absolute_native_units(backend.cb, l.value) absolute_native_units(backend::ImmerseBackend, l::Tuple{Length{:mm},Length{:mm}}) = (absolute_native_units(backend.cb, l[1].value), absolute_native_units(backend.cb, l[2].value)) surface(backend::ImmerseBackend) = surface(backend.cb) draw(backend::ImmerseBackend, form::Form) = draw(backend.cb, form) finish(backend::ImmerseBackend) = finish(backend.cb) iswithjs(::ImmerseBackend) = false iswithousjs(::ImmerseBackend) = true ================================================ FILE: src/list.jl ================================================ # Basic list abstract type List{T} end struct ListNull{T} <: List{T} end copy(l::ListNull{T}) where {T} = l mutable struct ListNode{T} <: List{T} head::T tail::List{T} end copy(l::ListNode{T}) where {T} = ListNode{T}(l.head, l.tail) head(l::ListNode) = l.head tail(l::ListNode) = l.tail # iterator function Base.iterate(l::List, state=l) return typeof(state) <: ListNull ? nothing : (state.head, state.tail) end cons(value, l::List{T}) where T = ListNode{T}(value, l) function length(l::List{T}) where T n = 0 while typeof(l) != ListNull{T} n += 1 l = l.tail end n end function cat(a::List{T}, b::List{T}) where T if a === nothing b else a = copy(a) u = a while u.tail !== nothing u.tail = copy(u.tail) u = u.tail end u.tail = b a end end function show(io::IO, a::List{T}) where T print(io, "List([") while typeof(a) != ListNull{T} print(io, a.head) if typeof(a.tail) != ListNull{T} print(io, ", ") end a = a.tail end print(io, "])") end ================================================ FILE: src/measure.jl ================================================ using Measures: Add, Min, Max, Div, Mul, Neg using LinearAlgebra # Measure Constants # ----------------- const cx = Length{:cx} const cy = Length{:cy} const sx = Length{:sx} const sy = Length{:sy} *(a::T, b::Type{cx}) where T = x_measure(a) *(a::T, b::Type{cy}) where T = y_measure(a) *(a::T, b::Type{sx}) where T = size_x_measure(a) *(a::T, b::Type{sy}) where T = size_y_measure(a) # Pixels are not typically used in Compose in preference of absolute # measurements or measurements relative to parent canvases. So for the # 'px' constant, we just punt and give something do something vaguely # reasonable. const assumed_ppmm = 3.78 # equivalent to 96 DPI const px = mm/assumed_ppmm const MeasureOrNumber = Union{Measure, Number} const XYTupleOrVec = Union{Tuple{MeasureOrNumber,MeasureOrNumber}, Vec} # Scaling w and h components # -------------------------- # Compute the length of the given type. sum_component(::Type{T}, l) where T <: Length = 0.0 sum_component(::Type{T}, l::T) where T <: Length = l.value sum_component(::Type{T}, l::Add) where T <: Length = sum_component(T, l.a) + sum_component(T, l.b) # Scale a length component by some factor. scale_component(::Type{T}, scale, l) where T <: Length = l scale_component(::Type{T}, scale, l::T) where T <: Length = T(scale * l.value) scale_component(::Type{T}, scale, l::Add) where T <: Length = scale_component(T, scale, l.a) + scale_component(T, scale, l.b) # Interpretation of bare numbers # ------------------------------ x_measure(a::Measure) = a x_measure(a::T) where T = Length{:cx, T}(a) y_measure(a::Measure) = a y_measure(a::T) where T = Length{:cy, T}(a) size_x_measure(a::Measure) = a size_x_measure(a::T) where T = Length{:sx, T}(a) size_y_measure(a::Measure) = a size_y_measure(a::T) where T = Length{:sy, T}(a) x_measure(a::Vector{T}) where T <: Measure = a x_measure(a::Vector) = Measure[x_measure(x) for x in a] y_measure(a::Vector{T}) where T <: Measure = a y_measure(a::Vector) = Measure[y_measure(y) for y in a] size_x_measure(a::Vector{T}) where T <: Measure = a size_x_measure(a::Vector) = Measure[size_x_measure(x) for x in a] size_y_measure(a::Vector{T}) where T <: Measure = a size_y_measure(a::Vector) = Measure[size_y_measure(y) for y in a] size_measure(a::Measure) = a size_measure(a) = a * mm x_measure(a::Missing) = x_measure(NaN) y_measure(a::Missing) = y_measure(NaN) size_x_measure(a::Missing) = size_x_measure(NaN) size_y_measure(a::Missing) = size_y_measure(NaN) # Higher-order measures # --------------------- # Compute the union of two bounding boxes. # # In other words, given two bounding boxes, return a new bounding box that # contains both. # # Unfortunately this is in general uncomputable without knowing the absolute # size of the parent canvas which may be passed in via the last two parameters. # If not passed, this throws an error if they would have been required. # function union(a::BoundingBox, b::BoundingBox, units=nothing, parent_abs_width=nothing, parent_abs_height=nothing) (a.width == Measure() || a.height == Measure()) && return b (b.width == Measure() || b.height == Measure()) && return a x0 = min(a.x0, b.x0) y0 = min(a.y0, b.y0) x1 = max(a.x0 + a.width, b.x0 + b.width) y1 = max(a.y0 + a.height, b.y0 + b.height) # Check whether we had any problematic computations for m in (x0,y0,x1,y1) # Pure absolute or pure relative points are fine. When they are mixed, # there are problems if !isabsolute(m) && m.abs != 0.0 units == nothing || parent_abs_width == nothing || parent_abs_height == nothing && error("""Bounding boxes are uncomputable without knowledge of the absolute dimensions of the top canvase due to mixing of relative and absolute coordinates. Either pass the dimension as a parameter or restrict the context to one kind of coordinates.""") parent_box = AbsoluteBox(0.0,0.0,parent_abs_width,parent_abs_height) abb = union(absolute_units(a,IdentityTransform(),units,parent_box), absolute_units(b,IdentityTransform(),units,parent_box)) return BoundingBox(Measure(;abs = abb.x0), Measure(;abs = abb.x0), Measure(;abs = abb.width), Measure(;abs = abb.height)) end end return BoundingBox(x0, y0, x1 - x0, y1 - y0) end function union(a::AbsoluteBox, b::AbsoluteBox) (a.width == 0.0 || a.height == 0.0) && return b (b.width == 0.0 || b.height == 0.0) && return a x0 = min(a.x0, b.x0) y0 = min(a.y0, b.y0) x1 = max(a.x0 + a.width, b.x0 + b.width) y1 = max(a.y0 + a.height, b.y0 + b.height) return AbsoluteBox(x0, y0, x1 - x0, y1 - y0) end # The same type-signature is used for a box used to assign # a custom coordinate system to a canvas. struct UnitBox{S,T,U,V} x0::S y0::T width::U height::V leftpad::AbsoluteLength rightpad::AbsoluteLength toppad::AbsoluteLength bottompad::AbsoluteLength end """ UnitBox(x0, y0, width, height; leftpad=0mm, rightpad=0mm, toppad=0mm, bottompad=0mm) Specifies the coordinate system for a [`context`](@ref), with origin `x0`, `y0`, plus `width`, `height`. """ UnitBox(x0, y0, width, height; leftpad=0mm, rightpad=0mm, toppad=0mm, bottompad=0mm) = UnitBox{typeof(x0), typeof(y0), typeof(width), typeof(height)}( x0, y0, width, height, leftpad, rightpad, toppad, bottompad) """ UnitBox(width, height; leftpad=0mm, rightpad=0mm, toppad=0mm, bottompad=0mm) Specifies the coordinate system for a [`context`](@ref), with origin `0, 0` plus `width`, `height`. """ function UnitBox(width, height; leftpad=0mm, rightpad=0mm, toppad=0mm, bottompad=0mm) S, T = typeof(width), typeof(height) UnitBox{S,T,S,T}(zero(S), zero(T), width, height, leftpad, rightpad, toppad, bottompad) end """ UnitBox() = UnitBox(0.0, 0.0, 1.0, 1.0) """ UnitBox() = UnitBox(0.0, 0.0, 1.0, 1.0) # copy with substitution UnitBox(units::UnitBox; x0=nothing, y0=nothing, width=nothing, height=nothing, leftpad=nothing, rightpad=nothing, toppad=nothing, bottompad=nothing) = UnitBox(ifelse(x0 === nothing, units.x0, x0), ifelse(y0 === nothing, units.y0, y0), ifelse(width === nothing, units.width, width), ifelse(height === nothing, units.height, height), leftpad = ifelse(leftpad === nothing, units.leftpad, leftpad), rightpad = ifelse(rightpad === nothing, units.rightpad, rightpad), toppad = ifelse(toppad === nothing, units.toppad, toppad), bottompad = ifelse(bottompad === nothing, units.bottompad, bottompad)) Measures.width(units::UnitBox) = units.width Measures.height(units::UnitBox) = units.height ispadded(units::UnitBox) = units.leftpad != 0mm || units.rightpad != 0mm || units.toppad != 0mm || units.bottompad != 0mm isxflipped(units::UnitBox{S, T, U, V}) where {S, T, U, V} = units.width < zero(U) isyflipped(units::UnitBox{S, T, U, V}) where {S, T, U, V} = units.height < zero(V) hasunits(::Type, x::Measure) = false hasunits(::Type{Length{u}}, x::Length{u, T}) where {u, T} = true hasunits(T::Type, x::Measures.BinaryOp) = hasunits(T, x.a) || hasunits(T, x.b) # Canvas Transforms # ----------------- # Transform matrix in absolute coordinates abstract type Transform end struct IdentityTransform <: Transform end struct MatrixTransform <: Transform M::Matrix{Float64} end combine(a::IdentityTransform, b::IdentityTransform) = a combine(a::IdentityTransform, b::MatrixTransform) = b combine(a::MatrixTransform, b::IdentityTransform) = a combine(a::MatrixTransform, b::MatrixTransform) = MatrixTransform(a.M * b.M) # Rotation about a point. struct Rotation{P <: Vec} theta::Float64 offset::P end Rotation(theta::Number, offset::XYTupleOrVec) = Rotation(convert(Float64, theta), (x_measure(offset[1]), y_measure(offset[2]))) """ Rotation(θ, x, y) Rotate all forms in context around point `(x,y)` by angle `θ` in radians. """ Rotation(theta::Number, offset_x, offset_y) = Rotation(convert(Float64, theta), (x_measure(offset_x), y_measure(offset_y))) """ Rotation(θ) `Rotation(θ)=Rotation(θ, 0.5w, 0.5h)` """ Rotation(theta::Number) = Rotation{Vec2}(convert(Float64, theta), (0.5w, 0.5h)) Rotation() = Rotation(0.0, (0.5w, 0.5h)) copy(rot::Rotation) = Rotation(rot) function convert(::Type{Transform}, rot::Rotation) if rot.theta == 0.0 return IdentityTransform() else ct = cos(rot.theta) st = sin(rot.theta) x0 = rot.offset[1] - (ct * rot.offset[1] - st * rot.offset[2]) y0 = rot.offset[2] - (st * rot.offset[1] + ct * rot.offset[2]) return MatrixTransform([ct -st x0.value st ct y0.value 0.0 0.0 1.0]) end end # Mirror about a point at a given angle mutable struct Mirror theta::Float64 point::Vec end """ Mirror(θ, x, y) Mirror line passing through point `(x,y)` at angle `θ` (in radians). """ Mirror(theta::Number, offset_x, offset_y) = Mirror(convert(Float64, theta), (offset_x, offset_y)) Mirror(theta::Number, offset::XYTupleOrVec) = Mirror(convert(Float64, theta), (x_measure(offset[1]), y_measure(offset[2]))) """ Mirror(θ) `Mirror(θ)=Mirror(θ, 0.5w, 0.5h)` """ Mirror(theta::Number) = Mirror(convert(Float64, theta), 0.5w, 0.5h) Mirror() = Mirror(0.0, (0.5w, 0.5h)) # copy constructor Mirror(mir::Mirror) = Mirror(copy(mir.theta), copy(mir.offset)) function convert(::Type{Transform}, mir::Mirror) n = [cos(mir.theta), sin(mir.theta)] x0 = mir.point[1] y0 = mir.point[2] offset = (2I - 2n*n') * [x0.value, y0.value] scale = (2n*n' - I) M = vcat(hcat(scale, offset), [0 0 1]) return MatrixTransform(M) end copy(mir::Mirror) = Mirror(mir) # Resolution # ---------- resolve(box::AbsoluteBox, units::UnitBox, t::Transform, a::Length) = resolve(box, a) resolve_position(box::AbsoluteBox, units::UnitBox, t::Transform, a::Length{:cx}) = ((a.value - units.x0) / width(units)) * box.a[1] resolve(box::AbsoluteBox, units::UnitBox, t::Transform, a::Length{:cx}) = abs(a.value / width(units)) * box.a[1] resolve_position(box::AbsoluteBox, units::UnitBox, t::Transform, a::Length{:cy}) = ((a.value - units.y0) / height(units)) * box.a[2] resolve(box::AbsoluteBox, units::UnitBox, t::Transform, a::Length{:cy}) = abs(a.value / height(units)) * box.a[2] resolve(box::AbsoluteBox, units::UnitBox, t::Transform, a::Length{:sx}) = a.value / width(units) * box.a[1] resolve(box::AbsoluteBox, units::UnitBox, t::Transform, a::Length{:sy}) = a.value / height(units) * box.a[2] function resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::Vec2) xy = (resolve_position(box, units, t, p[1]) + box.x0[1], resolve_position(box, units, t, p[2]) + box.x0[2]) return xy end function resolve(box::AbsoluteBox, units::UnitBox, t::MatrixTransform, p::Vec2) x = resolve_position(box, units, t, p[1]) + box.x0[1] y = resolve_position(box, units, t, p[2]) + box.x0[2] xy = t.M * [x.value, y.value, 1] return (xy[1]mm, xy[2]mm) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, a::BoundingBox) = BoundingBox(resolve(box, units, IdentityTransform(), a.x0), (resolve(box, units, t, a.a[1]), resolve(box, units, t, a.a[2]))) resolve(box::AbsoluteBox, units::UnitBox, t::Transform, a::Rotation) = Rotation(a.theta, resolve(box, units, t, a.offset)) resolve(box::AbsoluteBox, units::UnitBox, t::Transform, a::Mirror) = Mirror(a.theta, resolve(box, units, t, a.point)) function resolve(box::AbsoluteBox, units::UnitBox, t::Transform, u::UnitBox) if !ispadded(u) return u else leftpad = resolve(box, units, t, u.leftpad) rightpad = resolve(box, units, t, u.rightpad) toppad = resolve(box, units, t, u.toppad) bottompad = resolve(box, units, t, u.bottompad) # just give up trying to pad the units if it's impossible if leftpad + rightpad >= box.a[1] || toppad + bottompad >= box.a[2] return UnitBox(u.x0, u.y0, u.width, u.height) end width = u.width * (box.a[1] / (box.a[1] - leftpad - rightpad)) height = u.height * (box.a[2] / (box.a[2] - toppad - bottompad)) x0 = u.x0 - width * (leftpad / box.a[1]) y0 = u.y0 - height * (toppad / box.a[2]) return UnitBox(x0, y0, width, height) end end # Equivalent to the resolve functions in Measures, but pass through the `units` # and `transform` parameters. resolve(box::AbsoluteBox, units::UnitBox, t::Transform, x::Neg) = -resolve(box, units, t, x.a) resolve(box::AbsoluteBox, units::UnitBox, t::Transform, x::Add) = resolve(box, units, t, x.a) + resolve(box, units, t, x.b) resolve(box::AbsoluteBox, units::UnitBox, t::Transform, x::Mul) = resolve(box, units, t, x.a) * x.b resolve(box::AbsoluteBox, units::UnitBox, t::Transform, x::Div) = resolve(box, units, t, x.a) / x.b resolve(box::AbsoluteBox, units::UnitBox, t::Transform, x::Min) = min(resolve(box, units, t, x.a), resolve(box, units, t, x.b)) resolve(box::AbsoluteBox, units::UnitBox, t::Transform, x::Max) = max(resolve(box, units, t, x.a), resolve(box, units, t, x.b)) resolve_position(box::AbsoluteBox, units::UnitBox, t::Transform, a) = resolve(box, units, t, a) resolve_position(box::AbsoluteBox, units::UnitBox, t::Transform, op::Add) = resolve_position(box, units, t, op.a) + resolve_position(box, units, t, op.b) # Shear about a point at a given angle struct Shear shear::Float64 theta::Float64 point::Vec end """ Shear(s, θ, x, y) Shear line passing through point `(x,y)` at angle `θ` (in radians), with shear `s`. """ Shear(shear::Number, theta::Number, offset_x, offset_y) = Shear(float(shear), float(theta), (offset_x, offset_y)) Shear(shear::Number, theta::Number, offset::XYTupleOrVec) = Shear(float(shear), float(theta), (x_measure(offset[1]), y_measure(offset[2]))) """ Shear(s, θ) `Shear(s, θ)=Shear(s, θ, 0.5w, 0.5h)` """ Shear(shear::Number, theta::Number) = Shear(float(shear), float(theta), 0.5w, 0.5h) function convert(::Type{Transform}, shear::Shear) x, y = shear.point[1].value, shear.point[2].value ct, st = cos(shear.theta), sin(shear.theta) M1 = [1.0 0 x; 0 1 y; 0 0 1 ] M2 = I + shear.shear .* [ct*st -ct*ct 0; st*st -ct*st 0; 0 0 0.0] M3 = [1.0 0 -x; 0 1 -y; 0 0 1 ] M = M1*M2*M3 return MatrixTransform(M) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, a::Shear) = Shear(a.shear, a.theta, resolve(box, units, t, a.point)) # for transforming quadratic to cubic controlpnt(anchor::XYTupleOrVec, control::XYTupleOrVec) = 0.333*anchor + 0.667*control ================================================ FILE: src/misc.jl ================================================ iszero(x::T) where T = x == zero(T) Maybe(T::Type) = Union{T, Nothing} function in_expr_args(ex::Expr) ex.head === :in && return ex.args[1], ex.args[2] (ex.head === :call && length(ex.args) == 3 && ex.args[1] === :in) && return ex.args[2], ex.args[3] error("Not an `in` expression") end # Cycle-zip. Zip two or more arrays, cycling the short ones. function cyclezip(xs::AbstractArray...) any(map(isempty, xs)) && return Any[] n = maximum([length(x) for x in xs]) return takestrict(zip([Iterators.cycle(x) for x in xs]...), n) end # This generates optimized code for a reoccuring pattern in forms and patterns # that looks like: # # return Circle([CirclePrimitive((x, y), x_measure(r)) # for (x, y, r) in cyclezip(xs, ys, rs)]) # # This macro does the equivalent with # # return @makeform (x in xs, y in ys, r in rs), # CirclePrimitive((x, y), x_measure(r))) # # but much more efficiently. macro makeform(args...) @assert 1 <= length(args) <= 2 tag = length(args) == 2 ? args[2] : empty_tag args = args[1] @assert args.head == :tuple @assert length(args.args) == 2 iterators, constructor = args.args maxlen_ex = quote begin n = 0 end end type_ex = quote begin end end iter_ex = quote begin end end for iterator in iterators.args var, arr = in_expr_args(iterator::Expr) ivar = Symbol(string("i_", var)) push!(maxlen_ex.args, quote n = max(n, length($(arr))) if isempty($(arr)) error("Form cannot be constructed from an empty array") end end) push!(type_ex.args, quote $(ivar) = 1 $(var) = $(arr)[1] end) push!(iter_ex.args, quote $(ivar) += 1 $(ivar) = $(ivar) > length($(arr)) ? 1 : $(ivar) $(var) = $(arr)[$(ivar)] end) end esc(quote $(maxlen_ex) $(type_ex) prim1 = $(constructor) T = typeof(prim1) primitives = Array{T}(undef, n) primitives[1] = prim1 for i in 2:n $(iter_ex) primitives[i] = $(constructor)::T end Form{T}(primitives, $(tag)) end) end # TODO: Remove after replacing usage in property.jl macro makeprimitives(args) @assert args.head == :tuple @assert length(args.args) == 3 T, iterators, constructor = args.args maxlen_ex = quote begin n = 0 end end iter_ex = quote begin end end for iterator in iterators.args var, arr = in_expr_args(iterator::Expr) push!(maxlen_ex.args, quote if isempty($(esc(arr))) primitives = Array{$(T)}(undef, 0) @goto done end end) push!(maxlen_ex.args, quote n = max(n, length($(esc(arr)))) end) push!(iter_ex.args, quote $(esc(var)) = $(esc(arr))[((i - 1) % length($(esc(arr)))) + 1] end) end quote $(maxlen_ex) primitives = Array{$(esc(T))}(undef, n) for i in 1:n $(iter_ex) primitives[i] = $(esc(constructor)) end @label done primitives end end narrow_polygon_point_types(point_arrays::AbstractArray{Vector{Tuple{XM, YM}}}) where {XM <: Measure, YM <: Measure} = (XM, YM) type_params(p::Type{Tuple{XM, YM}}) where {XM, YM} = (Any, Any) type_params(p::Type{Tuple{XM, YM}}) where {XM <: Measure, YM <: Measure} = (XM, YM) type_params(p::Type{Union{}}) = (Any, Any) function narrow_polygon_point_types(point_arrays::AbstractArray) if !isempty(point_arrays) && all([eltype(arr) <: Vec for arr in point_arrays]) xm, ym = type_params(eltype(point_arrays[1])) for i in 2:length(point_arrays) type_params(eltype(point_arrays[i])) == (xm, ym) || return Any, Any end return xm, ym else return Any, Any end end function narrow_polygon_point_types(ring_arrays::Vector{Vector{Vector{P}}}) where P <: Tuple type_params(p::Type{Tuple{XM, YM}}) where {XM, YM} = (XM, YM) xm = nothing ym = nothing for point_arrays in ring_arrays if !isempty(point_arrays) && all([eltype(arr) <: Vec for arr in point_arrays]) if xm == nothing xm, ym = type_params(eltype(point_arrays[1])) end for i in 2:length(point_arrays) type_params(eltype(point_arrays[i])) == (xm, ym) || Any, Any end end end if xm == nothing return Any, Any else return xy, ym end end # Hacks to make Dates time work as coordinates if !hasmethod(/, (Dates.Day, Dates.Day)) /(a::Dates.Day, b::Dates.Day) = a.value / b.value end if !hasmethod(/, (Dates.Day, Real)) /(a::Dates.Day, b::Real) = Dates.Day(round(Int64, (a.value / b))) end /(a::Dates.Day, b::AbstractFloat) = convert(Dates.Millisecond, a) / b if !hasmethod(/, (Dates.Millisecond, Dates.Millisecond)) /(a::Dates.Millisecond, b::Dates.Millisecond) = a.value / b.value end if !hasmethod(/, (Dates.Millisecond, Real)) /(a::Dates.Millisecond, b::Real) = Dates.Millisecond(round(Int64, (a.value / b))) end /(a::Dates.Millisecond, b::AbstractFloat) = Dates.Millisecond(round(Int64, (a.value / b))) if !hasmethod(-, (Dates.Date, Dates.DateTime)) -(a::Dates.Date, b::Dates.DateTime) = convert(Dates.DateTime, a) - b end +(a::Dates.Date, b::Dates.Millisecond) = convert(Dates.DateTime, a) + b if !hasmethod(-, (Dates.DateTime, Dates.Date)) -(a::Dates.DateTime, b::Dates.Date) = a - convert(Dates.DateTime, b) end if !hasmethod(/, (Dates.Day, Dates.Millisecond)) /(a::Dates.Day, b::Dates.Millisecond) = convert(Dates.Millisecond, a) / b end if !hasmethod(/, (Dates.Millisecond, Dates.Day)) /(a::Dates.Millisecond, b::Dates.Day) = a / convert(Dates.Millisecond, b) end for T in [Dates.Hour, Dates.Minute, Dates.Second, Dates.Millisecond] if !hasmethod(-, (Dates.Date, T)) @eval begin -(a::Dates.Date, b::$(T)) = convert(Dates.DateTime, a) - b end end end *(a::AbstractFloat, b::Dates.Day) = Dates.Day(round(Int64, (a * b.value))) *(a::Dates.Day, b::AbstractFloat) = b * a *(a::AbstractFloat, b::Dates.Millisecond) = Dates.Millisecond(round(Int64, (a * b.value))) *(a::Dates.Millisecond, b::AbstractFloat) = b * a ================================================ FILE: src/pango.jl ================================================ # Estimation of text extents using pango. const libpangocairo = Cairo.libpangocairo const libpango = Cairo.libpango const libgobject = Cairo.libgobject # Cairo text backend const CAIRO_FONT_TYPE_TOY = 0 const CAIRO_FONT_TYPE_FT = 1 const CAIRO_FONT_TYPE_WIN32 = 2 const CAIRO_FONT_TYPE_QUARTZ = 3 const CAIRO_FONT_TYPE_USER = 4 # Mirroring a #define in the pango header. const PANGO_SCALE = 1024.0 # Use the freetype/fontconfig backend to find the best match to a font # description. # # Args: # desc: A string giving the font description. This can # also provide a comma-separated list of families. E.g., # "Helvetica, Arial 10" # # Returns: # A pointer to a PangoFontDescription with the closest match. # let available_font_families = Set{AbstractString}() for font_pattern in Fontconfig.list() push!(available_font_families, lowercase(Fontconfig.format(font_pattern, "%{family}"))) end meta_families = Set(["serif", "sans", "sans-serif", "monospace", "cursive", "fantasy"]) global match_font function match_font(families::AbstractString, size::Float64) matched_family = "sans-serif" for family in [lowercase(strip(family, [' ', '"', '\''])) for family in split(families, ',')] if family in available_font_families || family in meta_families matched_family = family break end end family = Fontconfig.format(match(Fontconfig.Pattern(family=matched_family)), "%{family}") desc = @sprintf("%s %fpx", family, size) fd = ccall((:pango_font_description_from_string, libpango), Ptr{Cvoid}, (Ptr{UInt8},), desc) return fd end end # Thin wrapper for a pango_layout object. mutable struct PangoLayout layout::Ptr{Cvoid} end function PangoLayout() layout = ccall((:pango_layout_new, libpango), Ptr{Cvoid}, (Ptr{Cvoid},), pango_cairo_ctx[]) # TODO: finalizer? PangoLayout(layout) end # Set the layout's font. function pango_set_font(pangolayout::PangoLayout, family::AbstractString, pts::Number) fd = match_font(family, pts) ccall((:pango_layout_set_font_description, libpango), Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}), pangolayout.layout, fd) end # Find the width and height of a string. # # Args: # pangolayout: a pango layout object, with font, etc, set. # text: a string we might like to draw. # # Returns: # A (width, height) tuple in absolute units. # function pango_text_extents(pangolayout::PangoLayout, text::AbstractString) textarray = convert(String, text) ccall((:pango_layout_set_markup, libpango), Cvoid, (Ptr{Cvoid}, Ptr{UInt8}, Int32), pangolayout.layout, textarray, sizeof(textarray)) extents = Array{Int32}(undef, 4) ccall((:pango_layout_get_extents, libpango), Cvoid, (Ptr{Cvoid}, Ptr{Int32}, Ptr{Int32}), pangolayout.layout, extents, C_NULL) width, height = (extents[3] / PANGO_SCALE)pt, (extents[4] / PANGO_SCALE)pt end # Find the minimum width and height needed to fit any of the given strings. # # (A "user-friendly" wrapper for pango_text_extents.) # # Args: # font_family: Something like a font name. # pts: Font size in points. # texts: One or more strings. # # Returns: # A (width, height) tuple in absolute units. # function max_text_extents(font_family::AbstractString, pts::Float64, texts::AbstractString...) pango_set_font(pangolayout[]::PangoLayout, font_family, pts) max_width = 0mm max_height = 0mm for text in texts (width, height) = pango_text_extents(pangolayout[]::PangoLayout, text) max_width = max_width.value < width.value ? width : max_width max_height = max_height.value < height.value ? height : max_height end return (max_width, max_height) end # Same as max_text_extents but with font_size in arbitrary absolute units. function max_text_extents(font_family::AbstractString, size::Measure, texts::AbstractString...) isa(size, AbsoluteLength) || error("text_extents requries font size be in absolute units") return max_text_extents(font_family, size/pt, texts...) end # Return an array with the extents of each element function text_extents(font_family::AbstractString, pts::Float64, texts::AbstractString...) pango_set_font(pangolayout[]::PangoLayout, font_family, pts) return [pango_text_extents(pangolayout[]::PangoLayout, text) for text in texts] end text_extents(font_family::AbstractString, size::Measure, texts::AbstractString...) = text_extents(font_family, size/pt, texts...) const pango_attrs = [ (:PANGO_ATTR_LANGUAGE, :PangoAttrLanguage), (:PANGO_ATTR_FAMILY, :PangoAttrString), (:PANGO_ATTR_STYLE, :PangoAttrInt), (:PANGO_ATTR_WEIGHT, :PangoAttrInt), (:PANGO_ATTR_VARIANT, :PangoAttrInt), (:PANGO_ATTR_STRETCH, :PangoAttrInt), (:PANGO_ATTR_SIZE, :PangoAttrSize), (:PANGO_ATTR_FONT_DESC, :PangoAttrFontDesc), (:PANGO_ATTR_FOREGROUND, :PangoAttrColor), (:PANGO_ATTR_BACKGROUND, :PangoAttrColor), (:PANGO_ATTR_UNDERLINE, :PangoAttrInt), (:PANGO_ATTR_STRIKETHROUGH, :PangoAttrInt), (:PANGO_ATTR_RISE, :PangoAttrInt), (:PANGO_ATTR_SHAPE, :PangoAttrShape), (:PANGO_ATTR_SCALE, :PangoAttrFloat), (:PANGO_ATTR_FALLBACK, :PangoAttrFallback), (:PANGO_ATTR_LETTER_SPACING, :PangoAttrInt), (:PANGO_ATTR_UNDERLINE_COLOR, :PangoAttrColor), (:PANGO_ATTR_ABSOLUTE_SIZE, :PangoAttrSize), (:PANGO_ATTR_GRAVITY, :PangoAttrInt), (:PANGO_ATTR_GRAVITY_HINT, :PangoAttrInt)] for (i, (attr, t)) in enumerate(pango_attrs) @eval begin const $attr = $i end end const PANGO_STYLE_NORMAL = 0 const PANGO_STYLE_OBLIQUE = 1 const PANGO_STYLE_ITALIC = 2 const PANGO_WEIGHT_THIN = 100 const PANGO_WEIGHT_ULTRALIGHT = 200 const PANGO_WEIGHT_LIGHT = 300 const PANGO_WEIGHT_BOOK = 380 const PANGO_WEIGHT_NORMAL = 400 const PANGO_WEIGHT_MEDIUM = 500 const PANGO_WEIGHT_SEMIBOLD = 600 const PANGO_WEIGHT_BOLD = 700 const PANGO_WEIGHT_ULTRABOLD = 800 const PANGO_WEIGHT_HEAVY = 900 const PANGO_WEIGHT_ULTRAHEAVY = 1000 # A Julia manifestation of a set of pango attributes mutable struct PangoAttr rise::Maybe(Int) scale::Maybe(Float64) style::Maybe(Int) weight::Maybe(Int) end PangoAttr() = PangoAttr(nothing, nothing, nothing, nothing) isempty(attr::PangoAttr) = all([getfield(attr, name) === nothing for name in fieldnames(PangoAttr)]) # Set an attribute in a PangoAttr # # Args: # attr: A PangoAttr to update. # attr_name: A pango attribute name (e.g., :PANGO_ATTR_RISE) # value: The value with which to update the attribute. # # Returns: # The attr. function update_pango_attr(attr::PangoAttr, attr_name::Symbol, value) if attr_name == :PANGO_ATTR_RISE attr.rise = Int64(value) elseif attr_name == :PANGO_ATTR_SCALE attr.scale = value elseif attr_name == :PANGO_ATTR_STYLE attr.style = Int64(value) elseif attr_name == :PANGO_ATTR_WEIGHT attr.weight = Int64(value) end attr end # Unpack the first part of a pango attribute # # Args: # ptr: A pointer to a PangoAttribute # t: The type of the attribute (e.g. PangoAttrInt) # # Returns: # A tuple of the form (start_idx, end_idx, value) # function unpack_pango_attr(ptr::Ptr{Cvoid}, t::Symbol) ptr += sizeof(Ptr{Cvoid}) # skip `klass` pointer ptr = convert(Ptr{UInt32}, ptr) idx = unsafe_wrap(Array, ptr, (2,), own=false) ptr += 2 * sizeof(UInt32) ptr = convert(Ptr{Cvoid}, ptr) if t == :PangoAttrInt value = unpack_pango_int(ptr) elseif t == :PangoAttrFloat value = unpack_pango_float(ptr) else value = nothing end (idx[1], idx[2], value) end # Unpack a pango int attribute. # # Args: # ptr: A point to a PangoAttrInt plus sizeof(PangoAttribute) # # Returns: # And int value. unpack_pango_int(ptr::Ptr{Cvoid}) = unsafe_wrap(Array, convert(Ptr{Int32}, ptr), (1,), own=false)[1] unpack_pango_float(ptr::Ptr{Cvoid}) = unsafe_wrap(Array, convert(Ptr{Float64}, ptr), (1,), own=false)[1] #function unpack_pango_size(ptr::Ptr{Cvoid}) #ptr = convert(Ptr{Int32}, ptr) #size = point_to_array(ptr, (1,))[1] #ptr = convert(Ptr{UInt32}, ptr) #absolute = point_to_array(ptr, (1,))[1] & 0x1 #println(size, absolute) #nothing #end # TODO: unpacking other attributes # Unpack a list of pango attributes # # Args: # ptr: A pointer to a PangoAttrList # # Returns: # A list of the form [(start_idx, attribute), ...] in which the start_idx # values are increasing and the attribute is a set of attributes that # should be applied starting at that position. # function unpack_pango_attr_list(ptr::Ptr{Cvoid}) attr_it = ccall((:pango_attr_list_get_iterator, libpango), Ptr{Cvoid}, (Ptr{Cvoid},), ptr) # Alias some ugly C calls. attr_it_next = () -> ccall((:pango_attr_iterator_next, libpango), Int32, (Ptr{Cvoid},), attr_it) attr_it_get = attr_name -> ccall((:pango_attr_iterator_get, libpango), Ptr{Cvoid}, (Ptr{Cvoid}, Int32), attr_it, eval(attr_name)) attr_it_range = () -> begin start_idx = Array{Int32}(undef, 1) end_idx = Array{Int32}(undef, 1) ccall((:pango_attr_iterator_range, libpango), Cvoid, (Ptr{Cvoid}, Ptr{Int32}, Ptr{Int32}), attr_it, start_idx, end_idx) (start_idx[1], end_idx[1]) end attrs = Array{Tuple{Int, PangoAttr}}(undef, 0) while attr_it_next() != 0 attr = PangoAttr() local start_idx for (attr_name, attr_type) in pango_attrs c_attr = attr_it_get(attr_name) (start_idx, end_idx) = attr_it_range() if c_attr != C_NULL (_, _, value) = unpack_pango_attr(c_attr, attr_type) update_pango_attr(attr, attr_name, value) end end push!(attrs, (start_idx, attr)) end ccall((:pango_attr_iterator_destroy, libpango), Cvoid, (Ptr{Cvoid},), attr_it) attrs end function pango_to_svg(text::AbstractString) # TODO: do c_stripped_text and c_attr_list need to be freed? c_stripped_text = Ref{Ptr{UInt8}}() c_attr_list = Ref{Ptr{Cvoid}}() output = IOBuffer() output_line = IOBuffer() textlines = split(text, "\n") carriage_shift = carriage_shift0 for (itextline,textline) in enumerate(textlines) ret = ccall((:pango_parse_markup, libpango), Int32, (Cstring, Int32, UInt32, Ptr{Ptr{Cvoid}}, Ptr{Ptr{UInt8}}, Ptr{UInt32}, Ptr{Cvoid}), textline, -1, 0, c_attr_list, c_stripped_text, C_NULL, C_NULL) ret == 0 && error("Could not parse pango markup.") input = codeunits(unsafe_string(c_stripped_text[])) lastpos = 1 baseline_shift = 0.0 sup = sub = false open_tag = false for (idx, attr) in unpack_pango_attr_list(c_attr_list[]) write(output_line, input[lastpos:idx]) lastpos = idx + 1 closing_tag = isempty(attr) open_tag && !closing_tag && write(output_line, "") if closing_tag write(output_line, "") else write(output_line, "") end if closing_tag && baseline_shift != 0.0 if lastpos < length(input) @printf(output_line, "", -baseline_shift) baseline_shift = 0.0 open_tag = true else open_tag = false end end end write(output_line, input[lastpos:end]) open_tag && write(output_line, "") itextline>1 && @printf(output, "", carriage_shift + sup*carriage_shift_supsub) write(output, String(take!(output_line))) itextline>1 && write(output, "") carriage_shift = carriage_shift0 - baseline_shift + sub*carriage_shift_supsub end String(take!(output)) end ================================================ FILE: src/pgf_backend.jl ================================================ mutable struct PGFPropertyFrame # Vector properties in this frame. vector_properties::Dict{Type, Property} # True if this property frame has scalar properties. Scalar properties are # emitted as a group {scope} that must be closed when the frame is popped. has_scalar_properties::Bool end PGFPropertyFrame() = PGFPropertyFrame(Dict{Type, Property}(), false) mutable struct PGF <: Backend # Image size in millimeters. width::AbsoluteLength height::AbsoluteLength # Output stream. out::IO # Fill properties cannot be "cleanly" applied to # multiple form primitives. It must be applied # each time an object is drawn fill::Union{Color, Nothing} fill_opacity::Float64 stroke::Union{Color, Nothing} stroke_opacity::Float64 fontfamily::Union{AbstractString, Nothing} fontsize::Float64 # Current level of indentation. indentation::Int # Output buffer. We want the ability to add to the beginning of buf::IOBuffer # Skip drawing if visible is false visible::Bool # Have not found an easy way to define color as # a draw parameter. Whenever we encounter a color, we add it to the # color_set set. That way, we can write out all the color # definitions at the same time. color_set::Set{Color} # Stack of property frames (groups of properties) currently in effect. property_stack::Vector{PGFPropertyFrame} # SVG forbids defining the same property twice, so we have to keep track # of which vector property of which type is in effect. If two properties of # the same type are in effect, the one higher on the stack takes precedence. vector_properties::Dict{Type, Union{Property, Nothing}} # Clip-paths that need to be defined at the end of the document. # Not quite sure how to deal with clip paths yet clippath::Union{ClipPrimitive, Nothing} # clippaths::Dict{ClipPrimitive, String} # True when finish has been called and no more drawing should occur finished::Bool # Backend is responsible for opening/closing the file ownedfile::Bool # Filename when ownedfile is true filename::Union{AbstractString, Nothing} # Emit the graphic on finish when writing to a buffer. emit_on_finish::Bool # Emit only the tikzpicture environment only_tikz::Bool # Use default TeX fonts instead of fonts specified by the theme. texfonts::Bool end function PGF(out::IO, width::AbsoluteLength, height::AbsoluteLength, emit_on_finish::Bool=true, only_tikz = false; texfonts = false, fill = default_fill_color, fill_opacity = 1.0, stroke = default_stroke_color, stroke_opacity = 1.0, fontfamily = nothing, fontsize = 12.0, indentation = 0, buf = IOBuffer(), visible = true, color_set = Set{Color}([colorant"black"]), property_stack = Array{PGFPropertyFrame}(undef, 0), vector_properties = Dict{Type, Union{Property, Nothing}}(), clippath = nothing, finished = false, ownedfile = false, filename = nothing) PGF(width, height, out, fill, fill_opacity, stroke, stroke_opacity, fontfamily, fontsize, indentation, buf, visible, color_set, property_stack, vector_properties, clippath, finished, ownedfile, filename, emit_on_finish, only_tikz, texfonts) end # Write to a file. """ PGF([output::Union{IO,AbstractString}], width=√200cm, height=10cm, only_tikz=false; texfonts=false) -> Backend Create a Portable Graphics Format backend. The output is normally passed to [`draw`](@ref). Specify a filename using a string as the first argument. If `only_tikz` is true then the output is a "tikzpicture", otherwise the output is a complete latex document with headers and footers. If `texfonts` is false, include "\\usepackage{fontspec}" in the headers. # Examples ``` c = compose(context(), rectangle()) draw(PGF("myplot.tex", 10cm, 5cm, true, texfonts=true), c) ``` """ PGF(filename::AbstractString, width=default_graphic_width, height=default_graphic_height, only_tikz=false; texfonts=false) = PGF(open(filename, "w"), width, height, true, only_tikz; texfonts = texfonts, ownedfile=true, filename=filename) # Write to buffer. PGF(width::MeasureOrNumber=default_graphic_width, height::MeasureOrNumber=default_graphic_height, emit_on_finish::Bool=true, only_tikz=false; texfonts=false) = PGF(IOBuffer(), width, height, emit_on_finish, only_tikz, texfonts=texfonts) function (img::PGF)(x) draw(img, x) end function finish(img::PGF) img.finished && return while !isempty(img.property_stack) pop_property_frame(img) end # if length(img.clippaths) > 0 # for (clippath, id) in img.clippaths # write(img.out, "\\clip") # print_svg_path(img.out, clippath.points) # write(img.out, ";\n") # end # end writeheader(img) writecolors(img) write(img.out, take!(img.buf)) write(img.out, """ \\end{tikzpicture} """ ) !img.only_tikz && write(img.out, """ \\end{document} """ ) hasmethod(flush, (typeof(img.out),)) && flush(img.out) close(img.buf) img.ownedfile && close(img.out) img.finished = true # If we are writing to a buffer. Collect the string and emit it. img.emit_on_finish && typeof(img.out) == IOBuffer && display(img) end isfinished(img::PGF) = img.finished root_box(img::PGF) = BoundingBox(0mm, 0mm, img.width, img.height) function writeheader(img::PGF) !img.only_tikz && write(img.out, """ \\documentclass{minimal} \\usepackage{pgfplots} $(img.texfonts ? "" : "\\usepackage{fontspec}") \\usepackage{amsmath} \\usepackage[active,tightpage]{preview} \\PreviewEnvironment{tikzpicture} \\begin{document} """) write(img.out, """ \\begin{tikzpicture}[x=1mm,y=-1mm] """) return img end function writecolors(img::PGF) for color in img.color_set @printf(img.out, "\\definecolor{mycolor%s}{rgb}{%s,%s,%s}\n", hex(color), svg_fmt_float(color.r), svg_fmt_float(color.g), svg_fmt_float(color.b) ) end end function print_pgf_path(out::IO, points::Vector{AbsoluteVec2}, bridge_gaps::Bool=false) isfirst = true for point in points x, y = point[1].value, point[2].value if !(isfinite(x) && isfinite(y)) isfirst = true continue end if isfirst isfirst = false @printf(out, " (%s,%s)", svg_fmt_float(x), svg_fmt_float(y)) else @printf(out, " -- (%s,%s)", svg_fmt_float(x), svg_fmt_float(y)) end end end function get_vector_properties(img::PGF, idx::Int) props_str = String[] modifiers = String[] for (propertytype, property) in img.vector_properties (property === nothing) && continue primitives = property.primitives idx > length(primitives) && error("Vector form and vector property differ in length. Can't distribute.") push_property!(props_str, img, primitives[idx]) end if img.fill !== nothing push!(props_str, string("fill=mycolor",hex(img.fill))) img.fill_opacity < 1.0 && push!(props_str, string("fill opacity=",svg_fmt_float(img.fill_opacity))) end if img.stroke !== nothing push!(props_str, string("draw=mycolor",hex(img.stroke))) img.stroke_opacity < 1.0 && push!(props_str, string("draw opacity=",svg_fmt_float(img.stroke_opacity))) end return modifiers, props_str end push_property!(props_str, img::PGF, property::StrokeDashPrimitive) = isempty(property.value) || push!(props_str, string("dash pattern=", join(map( v -> join(v, " "), zip(Iterators.cycle(["on", "off"]), map(v -> string(svg_fmt_float(v.value), "mm"), property.value)) )," "))) function push_property!(props_str, img::PGF, property::StrokePrimitive) if isa(property.color, TransparentColor) img.stroke = color(property.color) img.stroke_opacity = property.color.alpha else img.stroke = property.color end (img.stroke === nothing) || push!(img.color_set, convert(RGB, property.color)) end function push_property!(props_str, img::PGF, property::FillPrimitive) if isa(property.color, TransparentColor) img.fill = color(property.color) property.color.alpha<1.0 && (img.fill_opacity = property.color.alpha) else img.fill = property.color end (img.fill === nothing) || push!(img.color_set, convert(RGB, property.color)) end push_property!(props_str, img::PGF, property::VisiblePrimitive) = img.visible = property.value push_property!(props_str, img::PGF, property::LineWidthPrimitive) = push!(props_str, string("line width=", svg_fmt_float(property.value.value), "mm")) pgf_fmt_linecap(::LineCapButt) = "butt" pgf_fmt_linecap(::LineCapSquare) = "rect" pgf_fmt_linecap(::LineCapRound) = "round" push_property!(props_str, img::PGF, property::StrokeLineCapPrimitive) = push!(props_str, string("line cap=", pgf_fmt_linecap(property.value))) push_property!(props_str, img::PGF, property::StrokeLineJoinPrimitive) = push!(props_str, string("line join=", svg_fmt_linejoin(property.value))) push_property!(props_str, img::PGF, property::FillOpacityPrimitive) = img.fill_opacity = property.value push_property!(props_str, img::PGF, property::StrokeOpacityPrimitive) = img.stroke_opacity = property.value # Can only only work with one font family for now push_property!(props_str, img::PGF, property::FontPrimitive) = img.fontfamily = strip(split(escape_string(property.family),',')[1],'\'') push_property!(props_str, img::PGF, property::FontSizePrimitive) = img.fontsize = property.value.value # Not quite sure how to handle clipping yet, stub for now push_property!(props_str, img::PGF, property::ClipPrimitive) = img.clippath = property # Stubs for SVG and JS specific properties push_property!(props_str, img::PGF, property::JSIncludePrimitive) = nothing push_property!(props_str, img::PGF, property::JSCallPrimitive) = nothing push_property!(props_str, img::PGF, property::SVGClassPrimitive) = nothing push_property!(props_str, img::PGF, property::SVGAttributePrimitive) = nothing iswithjs(img::PGF) = false iswithousjs(img::PGF) = true push_property!(props_str, img::PGF, property::ArrowPrimitive) = push!(props_str, "arrows=->") # Form Drawing # ------------ function draw(img::PGF, form::Form) for (idx, primitive) in enumerate(form.primitives) draw(img, primitive, idx) end end function draw(img::PGF, prim::LinePrimitive, idx::Int) n = length(prim.points) n <= 1 && return modifiers, props = get_vector_properties(img, idx) img.visible || return write(img.buf, join(modifiers)) @printf(img.buf, "\\path [%s] ", join(props, ",")); print_pgf_path(img.buf, prim.points, true) write(img.buf, ";\n") end function draw(img::PGF, prim::RectanglePrimitive, idx::Int) width = max(prim.width.value, 0.01) height = max(prim.height.value, 0.01) modifiers, props = get_vector_properties(img, idx) img.visible || return write(img.buf, join(modifiers)) @printf(img.buf, "\\path [%s] ", join(props, ",")); @printf(img.buf, "(%s,%s) rectangle +(%s,%s);\n", svg_fmt_float(prim.corner[1].value), svg_fmt_float(prim.corner[2].value), svg_fmt_float(width), svg_fmt_float(height)) end function draw(img::PGF, prim::PolygonPrimitive, idx::Int) n = length(prim.points) n <= 1 && return modifiers, props = get_vector_properties(img, idx) img.visible || return write(img.buf, join(modifiers)) @printf(img.buf, "\\path [%s] ", join(props, ",")) print_pgf_path(img.buf, prim.points, true) write(img.buf, " -- cycle;\n") end function draw(img::PGF, prim::CirclePrimitive, idx::Int) modifiers, props = get_vector_properties(img, idx) img.visible || return write(img.buf, join(modifiers)) @printf(img.buf, "\\path [%s] ", join(props, ",")) @printf(img.buf, "(%s,%s) circle [radius=%s];\n", svg_fmt_float(prim.center[1].value), svg_fmt_float(prim.center[2].value), svg_fmt_float(prim.radius.value)) end function draw(img::PGF, prim::EllipsePrimitive, idx::Int) modifiers, props = get_vector_properties(img, idx) img.visible || return cx = prim.center[1].value cy = prim.center[2].value rx = sqrt((prim.x_point[1].value - cx)^2 + (prim.x_point[2].value - cy)^2) ry = sqrt((prim.y_point[1].value - cx)^2 + (prim.y_point[2].value - cy)^2) theta = rad2deg(atan(prim.x_point[2].value - cy, prim.x_point[1].value - cx)) all(isfinite,[cx, cy, rx, ry, theta]) || return modifiers, props = get_vector_properties(img, idx) write(img.buf, join(modifiers)) @printf(img.buf, "\\path [%s] ", join(props, ",")) @printf(img.buf, "(%s,%s) circle [x radius=%s, y radius=%s", svg_fmt_float(cx), svg_fmt_float(cy), svg_fmt_float(rx), svg_fmt_float(ry)) abs(theta) > 1e-4 && @printf(img.buf, " rotate=%s", svg_fmt_float(theta)) write(img.buf, "];\n") end function draw(img::PGF, prim::CurvePrimitive, idx::Int) modifiers, props = get_vector_properties(img, idx) img.visible || return write(img.buf, join(modifiers)) @printf(img.buf, "\\path [%s] ", join(props, ",")) @printf(img.buf, "(%s,%s) .. controls (%s,%s) and (%s,%s) .. (%s,%s);\n", svg_fmt_float(prim.anchor0[1].value), svg_fmt_float(prim.anchor0[2].value), svg_fmt_float(prim.ctrl0[1].value), svg_fmt_float(prim.ctrl0[2].value), svg_fmt_float(prim.ctrl1[1].value), svg_fmt_float(prim.ctrl1[2].value), svg_fmt_float(prim.anchor1[1].value), svg_fmt_float(prim.anchor1[2].value)) end function draw(img::PGF, prim::TextPrimitive, idx::Int) # Rotation direction is reversed! modifiers, props = get_vector_properties(img, idx) img.visible || return push!(props, string( "rotate around={", svg_fmt_float(-rad2deg(prim.rot.theta)), ": (", svg_fmt_float(prim.rot.offset[1].value - prim.position[1].value), ",", svg_fmt_float(prim.rot.offset[2].value - prim.position[2].value), ")}")) if prim.halign === hcenter push!(props, "inner sep=0.0") elseif prim.halign === hright push!(props, "left,inner sep=0.0") else push!(props, "right,inner sep=0.0") end write(img.buf, join(modifiers)) @printf(img.buf, "\\draw (%s,%s) node [%s]{\\fontsize{%smm}{%smm}\\selectfont \$%s\$};\n", svg_fmt_float(prim.position[1].value), svg_fmt_float(prim.position[2].value), replace(join(props, ","), "fill"=>"text"), svg_fmt_float(img.fontsize), svg_fmt_float(1.2*img.fontsize), pango_to_pgf(prim.value) ) end function indent(img::PGF) for i in 1:img.indentation write(img.buf, " ") end end function push_property_frame(img::PGF, properties::Vector{Property}) isempty(properties) && return frame = PGFPropertyFrame() applied_properties = Set{Type}() scalar_properties = Array{Property}(undef, 0) for property in properties if !isrepeatable(property) && (typeof(property) in applied_properties) continue elseif isscalar(property) push!(scalar_properties, property) push!(applied_properties, typeof(property)) frame.has_scalar_properties = true img.vector_properties[typeof(property)] = nothing else frame.vector_properties[typeof(property)] = property img.vector_properties[typeof(property)] = property end end push!(img.property_stack, frame) isempty(scalar_properties) && return write(img.buf, "\\begin{scope}\n") prop_str = AbstractString[] for property in scalar_properties push_property!(prop_str, img, property.primitives[1]) end if length(prop_str) > 0 @printf(img.buf, "[%s]\n", join(prop_str, ",")) end if img.clippath !== nothing write(img.buf, "\\clip ") print_pgf_path(img.buf, img.clippath.points) write(img.buf, ";\n") end if (!img.texfonts && (img.fontfamily !== nothing)) @printf(img.buf, "\\fontspec{%s}\n", img.fontfamily) end end function pop_property_frame(img::PGF) @assert !isempty(img.property_stack) frame = pop!(img.property_stack) if frame.has_scalar_properties write(img.buf, "\\end{scope}\n") # There should be a better way to to this: # Maybe put the applied properties on a stack then # pop them out here? # FIX ME! img.fill = default_fill_color img.stroke = default_stroke_color img.fill_opacity = 1.0 img.stroke_opacity = 1.0 img.fontfamily = nothing img.clippath = nothing img.visible = true end for (propertytype, property) in frame.vector_properties img.vector_properties[propertytype] = nothing for i in length(img.property_stack):-1:1 if haskey(img.property_stack[i].vector_properties, propertytype) img.vector_properties[propertytype] = img.property_stack[i].vector_properties[propertytype] end end end end # Horrible abuse of Latex inline math mode just to # get something working first. # FIX ME! function pango_to_pgf(text::AbstractString) pat = r"<(/?)\s*([^>]*)\s*>" input = codeunits(escape_tex_chars(text)) output = IOBuffer() lastpos = 1 for mat in eachmatch(pat, text) write(output, "\\text{") write(output, input[lastpos:mat.offset-1]) write(output, "}") if mat.captures[2] == "sup" if mat.captures[1] == "/" write(output, "}") else write(output, "^{") end elseif mat.captures[2] == "sub" if mat.captures[1] == "/" write(output, "}") else write(output, "_{") end elseif mat.captures[2] == "i" if mat.captures[1] == "/" write(output, "}") else write(output, "\\textit{") end elseif mat.captures[2] == "b" if mat.captures[1] == "/" write(output, "}") else write(output, "\\textbf{") end end lastpos = mat.offset + length(mat.match) end write(output, "\\text{") write(output, input[lastpos:end]) write(output, "}") return String(take!(output)) end function escape_tex_chars(text::AbstractString) # see http://www.cespedes.org/blog/85/how-to-escape-latex-special-characters escaped_str = text escaped_str = replace(escaped_str, "\\"=>"\\textbackslash{}") escaped_str = replace(escaped_str, "#"=>"\\#") escaped_str = replace(escaped_str, "\$"=>"\\\$") escaped_str = replace(escaped_str, "%"=>"\\%") escaped_str = replace(escaped_str, "&"=>"\\&") escaped_str = replace(escaped_str, "_"=>"\\_") escaped_str = replace(escaped_str, "{"=>"\\{") escaped_str = replace(escaped_str, "}"=>"\\}") escaped_str = replace(escaped_str, "^"=>"\\textasciicircum{}") escaped_str = replace(escaped_str, "~"=>"\\textasciitilde{}") escaped_str = replace(escaped_str, "\\textbackslash\\{\\}"=>"\\textbackslash{}") end function draw(img::PGF, prim::ArcPrimitive, idx::Int) angle2 = prim.angle2 + (prim.angle2 y return false end end end return false end struct Property{P <: PropertyPrimitive} <: ComposeNode primitives::Vector{P} end isempty(p::Property) = isempty(p.primitives) isscalar(p::Property) = length(p.primitives) == 1 # Some properties can be applied multiple times, most cannot. isrepeatable(p::Property) = false resolve(box::AbsoluteBox, units::UnitBox, t::Transform, p::Property{T}) where T = Property{T}([resolve(box, units, t, primitive) for primitive in p.primitives]) # Property primitive catchall: most properties don't need measure transforms resolve(box::AbsoluteBox, units::UnitBox, t::Transform, primitive::PropertyPrimitive) = primitive # Stroke # ------ struct StrokePrimitive <: PropertyPrimitive color::RGBA{Float64} end const Stroke = Property{StrokePrimitive} stroke(c::Nothing) = Stroke([StrokePrimitive(RGBA{Float64}(0, 0, 0, 0))]) stroke(c::Union{Colorant, AbstractString}) = Stroke([StrokePrimitive(parse_colorant(c))]) stroke(cs::AbstractArray) = Stroke([StrokePrimitive(c == nothing ? RGBA{Float64}(0, 0, 0, 0) : parse_colorant(c)) for c in cs]) prop_string(::Stroke) = "s" # Fill # ---- struct FillPrimitive <: PropertyPrimitive color::RGBA{Float64} end const Fill = Property{FillPrimitive} fill(c::Nothing) = Fill([FillPrimitive(RGBA{Float64}(0.0, 0.0, 0.0, 0.0))]) """ fill(c) Define a fill color, where `c` can be a `Colorant` or `String`. """ fill(c::Union{Colorant, AbstractString}) = Fill([FillPrimitive(parse_colorant(c))]) """ fill(cs::AbstractArray) Arguments can be passed in arrays in order to perform multiple drawing operations at once. """ fill(cs::AbstractArray) = Fill([FillPrimitive(c == nothing ? RGBA{Float64}(0.0, 0.0, 0.0, 0.0) : parse_colorant(c)) for c in cs]) prop_string(::Fill) = "f" # StrokeDash # ---------- struct StrokeDashPrimitive <: PropertyPrimitive value::Vector{Measure} end const StrokeDash = Property{StrokeDashPrimitive} strokedash(values::AbstractArray) = StrokeDash([StrokeDashPrimitive(collect(Measure, values))]) strokedash(values::AbstractArray{<:AbstractArray}) = StrokeDash([StrokeDashPrimitive(collect(Measure, value)) for value in values]) resolve(box::AbsoluteBox, units::UnitBox, t::Transform, primitive::StrokeDashPrimitive) = StrokeDashPrimitive([resolve(box, units, t, v) for v in primitive.value]) prop_string(::StrokeDash) = "sd" # StrokeLineCap # ------------- abstract type LineCap end struct LineCapButt <: LineCap end struct LineCapSquare <: LineCap end struct LineCapRound <: LineCap end struct StrokeLineCapPrimitive <: PropertyPrimitive value::LineCap end StrokeLineCapPrimitive(value::Type{LineCap}) = StrokeLineCapPrimitive(value()) const StrokeLineCap = Property{StrokeLineCapPrimitive} strokelinecap(value::Union{LineCap, Type{LineCap}}) = StrokeLineCap([StrokeLineCapPrimitive(value)]) strokelinecap(values::AbstractArray) = StrokeLineCap([StrokeLineCapPrimitive(value) for value in values]) prop_string(::StrokeLineCap) = "slc" # StrokeLineJoin # -------------- abstract type LineJoin end struct LineJoinMiter <: LineJoin end struct LineJoinRound <: LineJoin end struct LineJoinBevel <: LineJoin end struct StrokeLineJoinPrimitive <: PropertyPrimitive value::LineJoin end StrokeLineCapPrimitive(value::Type{LineJoin}) = new(value()) const StrokeLineJoin = Property{StrokeLineJoinPrimitive} strokelinejoin(value::Union{LineJoin, Type{LineJoin}}) = StrokeLineJoin([StrokeLineJoinPrimitive(value)]) strokelinejoin(values::AbstractArray) = StrokeLineJoin([StrokeLineJoinPrimitive(value) for value in values]) prop_string(::StrokeLineJoin) = "slj" # LineWidth # --------- struct LineWidthPrimitive <: PropertyPrimitive value::Measure function LineWidthPrimitive(value) return new(size_measure(value)) end end const LineWidth = Property{LineWidthPrimitive} linewidth(value::Union{Measure, Number}) = LineWidth([LineWidthPrimitive(value)]) linewidth(values::AbstractArray) = LineWidth([LineWidthPrimitive(value) for value in values]) resolve(box::AbsoluteBox, units::UnitBox, t::Transform, primitive::LineWidthPrimitive) = LineWidthPrimitive(resolve(box, units, t, primitive.value)) prop_string(::LineWidth) = "lw" # Visible # ------- struct VisiblePrimitive <: PropertyPrimitive value::Bool end const Visible = Property{VisiblePrimitive} visible(value::Bool) = Visible([VisiblePrimitive(value)]) visible(values::AbstractArray) = Visible([VisiblePrimitive(value) for value in values]) prop_string(::Visible) = "v" # FillOpacity # ----------- struct FillOpacityPrimitive <: PropertyPrimitive value::Float64 function FillOpacityPrimitive(value_::Number) value = Float64(value_) (value < 0.0 || value > 1.0) && error("Opacity must be between 0 and 1.") return new(value) end end const FillOpacity = Property{FillOpacityPrimitive} """ fillopacity(value) Define a fill opacity, where 0≤value≤1. For svg, nested contexts will inherit from parent contexts e.g. `(context(), fillopacity(a), (context(), fill(c::String), circle()))`. """ fillopacity(value::Float64) = FillOpacity([FillOpacityPrimitive(value)]) """ fillopacity(values::AbstractArray) Arguments can be passed in arrays in order to perform multiple drawing operations at once. """ fillopacity(values::AbstractArray) = FillOpacity([FillOpacityPrimitive(value) for value in values]) prop_string(::FillOpacity) = "fo" # StrokeOpacity # ------------- struct StrokeOpacityPrimitive <: PropertyPrimitive value::Float64 function StrokeOpacityPrimitive(value_::Number) value = Float64(value_) (value < 0.0 || value > 1.0) && error("Opacity must be between 0 and 1.") return new(value) end end const StrokeOpacity = Property{StrokeOpacityPrimitive} strokeopacity(value::Float64) = StrokeOpacity([StrokeOpacityPrimitive(value)]) strokeopacity(values::AbstractArray) = StrokeOpacity([StrokeOpacityPrimitive(value) for value in values]) prop_string(::StrokeOpacity) = "so" # Clip # ---- struct ClipPrimitive{P <: Vec} <: PropertyPrimitive points::Vector{P} end const Clip = Property{ClipPrimitive} clip() = Clip([ClipPrimitive(Array{Vec}(undef, 0))]) """ clip(points::AbstractArray) `clip()` is a property. Only forms inside the clip shape will be visible. """ function clip(points::AbstractArray{T}) where T <: XYTupleOrVec XM, YM = narrow_polygon_point_types(Vector[points]) if XM == Any XM = Length{:cx, Float64} end if YM == Any YM = Length{:cy, Float64} end VecType = Tuple{XM, YM} prim = ClipPrimitive(VecType[(x_measure(point[1]), y_measure(point[2])) for point in points]) return Clip(typeof(prim)[prim]) end """ clip(point_arrays::AbstractArray...) Arguments can be passed in arrays in order to perform multiple clipping operations at once. """ function clip(point_arrays::AbstractArray...) XM, YM = narrow_polygon_point_types(point_arrays) VecType = XM == YM == Any ? Vec : Vec{XM, YM} PrimType = XM == YM == Any ? ClipPrimitive : ClipPrimitive{VecType} clipprims = Array{PrimType}(undef, length(point_arrays)) for (i, point_array) in enumerate(point_arrays) clipprims[i] = ClipPrimitive(VecType[(x_measure(point[1]), y_measure(point[2])) for point in point_array]) end return Property{PrimType}(clipprims) end resolve(box::AbsoluteBox, units::UnitBox, t::Transform, primitive::ClipPrimitive) = ClipPrimitive{AbsoluteVec2}( AbsoluteVec2[ resolve(box, units, t, point) for point in primitive.points]) prop_string(::Clip) = "clp" # Font # ---- struct FontPrimitive <: PropertyPrimitive family::AbstractString end const Font = Property{FontPrimitive} font(family::AbstractString) = Font([FontPrimitive(family)]) font(families::AbstractArray) = Font([FontPrimitive(family) for family in families]) prop_string(::Font) = "fnt" Base.hash(primitive::FontPrimitive, h::UInt) = hash(primitive.family, h) ==(a::FontPrimitive, b::FontPrimitive) = a.family == b.family # FontSize # -------- struct FontSizePrimitive <: PropertyPrimitive value::Measure function FontSizePrimitive(value) return new(size_measure(value)) end end const FontSize = Property{FontSizePrimitive} fontsize(value::Union{Number, Measure}) = FontSize([FontSizePrimitive(value)]) fontsize(values::AbstractArray) = FontSize([FontSizePrimitive(value) for value in values]) resolve(box::AbsoluteBox, units::UnitBox, t::Transform, primitive::FontSizePrimitive) = FontSizePrimitive(resolve(box, units, t, primitive.value)) prop_string(::FontSize) = "fsz" # SVGID # ----- struct SVGIDPrimitive <: PropertyPrimitive value::AbstractString end const SVGID = Property{SVGIDPrimitive} svgid(value::AbstractString) = SVGID([SVGIDPrimitive(value)]) svgid(values::AbstractArray) = SVGID([SVGIDPrimitive(value) for value in values]) prop_string(::SVGID) = "svgid" Base.hash(primitive::SVGIDPrimitive, h::UInt) = hash(primitive.value, h) ==(a::SVGIDPrimitive, b::SVGIDPrimitive) = a.value == b.value # SVGClass # -------- struct SVGClassPrimitive <: PropertyPrimitive value::String end const SVGClass = Property{SVGClassPrimitive} svgclass(value::AbstractString) = SVGClass([SVGClassPrimitive(value)]) svgclass(values::AbstractArray) = SVGClass([SVGClassPrimitive(value) for value in values]) function prop_string(svgc::SVGClass) if isscalar(svgc) return string("svgc(", svgc.primitives[1].value, ")") else return string("svgc(", svgc.primitives[1].value, "...)") end end Base.hash(primitive::SVGClassPrimitive, h::UInt) = hash(primitive.value, h) ==(a::SVGClassPrimitive, b::SVGClassPrimitive) = a.value == b.value # SVGAttribute # ------------ struct SVGAttributePrimitive <: PropertyPrimitive attribute::String value::String end const SVGAttribute = Property{SVGAttributePrimitive} svgattribute(attribute::AbstractString, value) = SVGAttribute([SVGAttributePrimitive(attribute, string(value))]) svgattribute(attribute::AbstractString, values::AbstractArray) = SVGAttribute([SVGAttributePrimitive(attribute, string(value)) for value in values]) svgattribute(attributes::AbstractArray, values::AbstractArray) = SVGAttribute( @makeprimitives SVGAttributePrimitive, (attribute in attributes, value in values), SVGAttributePrimitive(attribute, string(value))) prop_string(::SVGAttribute) = "svga" function Base.hash(primitive::SVGAttributePrimitive, h::UInt) h = hash(primitive.attribute, h) h = hash(primitive.value, h) return h end ==(a::SVGAttributePrimitive, b::SVGAttributePrimitive) = a.attribute == b.attribute && a.value == b.value # JSInclude # --------- struct JSIncludePrimitive <: PropertyPrimitive value::AbstractString jsmodule::Union{Nothing, Tuple{AbstractString, AbstractString}} end const JSInclude = Property{JSIncludePrimitive} jsinclude(value::AbstractString, module_name=nothing) = JSInclude([JSIncludePrimitive(value, module_name)]) # Don't bother with a vectorized version of this. It wouldn't really make # # sense. prop_string(::JSInclude) = "jsip" # JSCall # ------ struct JSCallPrimitive <: PropertyPrimitive code::AbstractString args::Vector{Measure} end const JSCall = Property{JSCallPrimitive} jscall(code::AbstractString, arg::Vector{Measure}=Measure[]) = JSCall([JSCallPrimitive(code, arg)]) jscall(codes::AbstractArray, args::AbstractArray{Vector{Measure}}=Vector{Measure}[Measure[]]) = JSCall( @makeprimitives JSCallPrimitive, (code in codes, arg in args), JSCallPrimitive(code, arg)) function resolve(box::AbsoluteBox, units::UnitBox, t::Transform, primitive::JSCallPrimitive) # we are going to build a new string by scanning across "code" and # replacing %x with translated x values, %y with translated y values # and %s with translated size values. newcode = IOBuffer() i = 1 validx = 1 while true j = findnext(primitive.code, "%", i) if j === nothing write(newcode, primitive.code[i:end]) break end write(newcode, primitive.code[i:j-1]) if j == length(primitive.code) write(newcode, '%') break elseif primitive.code[j+1] == '%' write(newcode, '%') elseif primitive.code[j+1] == 'x' val = resolve(box, units, t, (primitive.args[validx], 0mm)) write(newcode, svg_fmt_float(val[1].value)) validx += 1 elseif primitive.code[j+1] == 'y' val = resolve(box, units, t, (0mm, primitive.args[validx])) write(newcode, svg_fmt_float(val[2].value)) validx += 1 elseif primitive.code[j+1] == 's' val = resolve(box, units, t, primitive.args[validx]) write(newcode, svg_fmt_float(val.value)) validx += 1 else write(newcode, '%', primitive.code[j+1]) end i = j + 2 end return JSCallPrimitive(String(take!(newcode)), Measure[]) end isrepeatable(p::JSCall) = true Base.isless(a::FillPrimitive, b::FillPrimitive) = color_isless(a.color, b.color) Base.isless(a::StrokePrimitive, b::StrokePrimitive) = color_isless(a.color, b.color) prop_string(::JSCall) = "jsc" # Arrow Property # ------- struct ArrowPrimitive <: PropertyPrimitive value::Bool end const Arrow = Property{ArrowPrimitive} """ arrow() `arrow() = arrow(true)` """ arrow() = Arrow([ArrowPrimitive(true)]) """ arrow(value::Bool) `arrow()` is a property of arcs, lines and curves. The color of the arrowhead is the same as `stroke()`, but for svg the results will be browser-dependent. """ arrow(value::Bool) = Arrow([ArrowPrimitive(value)]) """ arrow(values::AbstractArray) Arguments can be passed in arrays in order to perform multiple drawing operations at once. """ arrow(values::AbstractArray) = Arrow([ArrowPrimitive(value) for value in values]) prop_string(::Arrow) = "arrow" ================================================ FILE: src/stack.jl ================================================ # Convenience function for rearranging contexts # Create a new context containing the given contexts stacked horizontally. # # Args: # x0: X-position of the new root context # y0: Y-position of the new root context # height: Height of the root context. # aligned_contexts: One or more canvases accompanied with a vertical alignment # specifier, giving the vertical positioning of the context. # function hstack(x0, y0, height, aligned_contexts::(Tuple{Context, VAlignment})...) isempty(aligned_contexts) && return context(x0, y0, 0, height) widths = [aligned_context[1].box.a[1] for aligned_context in aligned_contexts] width = sum(widths) total_width_units = sum_component(Length{:w, Float64}, width) if total_width_units > 0.0 width -= total_width_units*w width += 1w end height = y_measure(height) root = context(x0, y0, width, height) x = 0w for (ctx, aln) in aligned_contexts ctx = copy(ctx) w_component = sum_component(Length{:w, Float64}, ctx.box.a[1]) box_w = ctx.box.a[1] if w_component != 0.0 box_w = scale_component(Length{:w, Float64}, w_component / total_width_units, ctx.box.a[1]) end y = ctx.box.x0[2] if aln == vtop y = 0h elseif aln == vcenter y = (height / 2) - (ctx.box.a[2] / 2) elseif aln == vbottom y = height - ctx.box.a[2] end ctx.box = BoundingBox((x, y), (box_w, ctx.box.a[2])) root = compose!(root, ctx) x += ctx.box.a[1] end return root end hstack() = context() # Create a new context containing the given contexts stacked horizontally. # # This is the simple version of hstack. The root context will be placed on 0cx, # 0cy, and its height will be the maximum of the contexts it contains. All # contexts will be centered vertically. # function hstack(contexts::Context...; x0::MeasureOrNumber=0, y0::MeasureOrNumber=0, height=0) if height == 0 height = maximum([context.box.a[2] for context in contexts]) end return hstack(x0, y0, height, [(context, vcenter) for context in contexts]...) end # Create a new context containing the given contexts stacked vertically. # # Args: # x0: X-position of the new root context # y0: Y-position of the new root context # width: Height of the root context. # aligned_contexts: One or more canvases accompanied with a horizontal alignment # specifier, giving the horizontal positioning of the context. # function vstack(x0, y0, width, aligned_contexts::(Tuple{Context, HAlignment})...) isempty(aligned_contexts) && return context(x0, y0, width, 0) heights = [aligned_context[1].box.a[2] for aligned_context in aligned_contexts] height = sum(heights) total_height_units = sum_component(Length{:h, Float64}, height) if total_height_units > 0.0 height -= total_height_units*h height += 1h end width = x_measure(width) root = context(x0, y0, width, height) y = 0h for (ctx, aln) in aligned_contexts ctx = copy(ctx) h_component = sum_component(Length{:h, Float64}, ctx.box.a[2]) box_h = ctx.box.a[2] if h_component != 0.0 box_h = scale_component(Length{:h, Float64}, h_component / total_height_units, ctx.box.a[2]) end x = ctx.box.x0[1] if aln == hleft x = 0w elseif aln == hcenter x = (width / 2) - (ctx.box.a[1] / 2) elseif aln == hright x = width - ctx.box.a[1] end ctx.box = BoundingBox((x, y), (ctx.box.a[1], box_h)) root = compose!(root, ctx) y += ctx.box.a[2] end return root end vstack() = context() # Create a new context containing the given contexts stacked horizontally. # # The simple version of vstack. The root context will be placed on 0cx, 0cy, and # its width will be the maximum of the contexts it contains. All contexts will # be centered horizontally.. # function vstack(contexts::Context...; x0::MeasureOrNumber=0, y0::MeasureOrNumber=0, width::MeasureOrNumber=0) if width == 0 width = maximum([context.box.a[1] for context in contexts]) end return vstack(x0, y0, width, [(context, hcenter) for context in contexts]...) end ================================================ FILE: src/svg.jl ================================================ using Base64 using UUIDs using Random const snapsvgjs = joinpath(@__DIR__, "..", "deps", "snap.svg-min.js") # Packages can insert extra XML namespaces here to be defined in the output # SVG. const xmlns = Dict() # All svg (in our use) coordinates are in millimeters. This number gives the # largest deviation from the true position allowed in millimeters. const eps = 0.01 # Format a floating point number into a decimal string of reasonable precision. function svg_fmt_float(x::Fractional) a = @sprintf("%0.8f", round(x / eps) * eps) n = length(a) while a[n] == '0' n -= 1 end if a[n] == '.' n -= 1 end a[1:n] end # A much faster version of svg_fmt_float. This does not allocate any # temporary buffers, because it writes directly to the output. function svg_print_float(io::IO, x::AbstractFloat) ndig = 2 if isfinite(x) if x < 0 write(io, '-') x = abs(x) end x = round(x/eps)*eps xt = trunc(UInt, x) dx = x - convert(Float64, xt) 0 <= dx < 1 || error("Formatting overflow") svg_print_uint(io, xt, 1) # width=1 prints 0.2 instead of .2 dxi = round(UInt, dx/eps) if dxi != 0 write(io, '.') svg_print_uint(io, dxi, ndig, true) end elseif isnan(x) write(io, "NaN") elseif x == Inf write(io, "Inf") elseif x == -Inf write(io, "-Inf") end end let a = Array{UInt8}(undef, 20) global svg_print_uint function svg_print_uint(io::IO, x::Unsigned, width = 0, drop = false) n = length(a) while x > 0 && n > 0 x, r = divrem(x, 10) a[n] = r n -= 1 end n == 0 && error("Formatting overflow") for i = 1:width-(length(a)-n) write(io, '0') end last = length(a) if drop while last > n && a[last] == 0 last -= 1 end end for i = n+1:last write(io, '0'+a[i]) end end end # Format a color for SVG. svg_fmt_color(c::Color) = string("#", hex(c)) svg_fmt_color(c::Nothing) = "none" # Javascript in a """) elseif img.jsmode == :linkabs write(img.out, """ """) elseif img.jsmode == :linkrel write(img.out, """ """) end if !isempty(img.scripts) || !isempty(img.jsheader) if img.jsmode == :embed write(img.out, " """) end write(img.out, " """) end write(img.out, "\n") end end write(img.out, "\n") hasmethod(flush, (typeof(img.out),)) && flush(img.out) img.ownedfile && close(img.out) img.finished = true # If we are writing to a buffer. Collect the string and emit it. img.emit_on_finish && typeof(img.out) == IOBuffer && display(img) end isfinished(img::SVG) = img.finished function show(io::IO, ::MIME"text/html", img::SVG) if img.cached_out === nothing img.cached_out = String(take!(img.out)) end write(io, """ $(img.cached_out) """) end function show(io::IO, ::MIME"image/svg+xml", img::SVG) if img.cached_out === nothing img.cached_out = String(take!(img.out)) end write(io, img.cached_out) end root_box(img::SVG) = BoundingBox(0mm, 0mm, img.width, img.height) function indent(img::SVG) for i in 1:img.indentation write(img.out, " ") end end # Draw # Generate SVG path data from an array of points. # # Args: # out: Output stream. # points: points on the path # bridge_gaps: when true, remove non-finite values, rather than forming # separate lines. (this arg is never used) # # Returns: # A string containing SVG path data. # function print_svg_path(out, points::Vector{AbsoluteVec2}) isfirst = true endp = points[end] for (currp, nxtp) in zip(points[1:end-1], points[2:end]) x1, y1 = currp[1].value, currp[2].value x2, y2 = nxtp[1].value, nxtp[2].value currentp = isfinite(x1) && isfinite(y1) nextp = isfinite(x2) && isfinite(y2) if (isfirst && currentp && nextp) write(out, 'M') svg_print_float(out, x1) write(out, ',') svg_print_float(out, y1) write(out, " L") isfirst = false elseif (!isfirst && currentp) svg_print_float(out, x1) write(out, ',') svg_print_float(out, y1) write(out, ' ') end !nextp && (isfirst = true) end svg_print_float(out, endp[1].value) write(out, ',') svg_print_float(out, endp[2].value) write(out, ' ') end # Property Printing # ----------------- function print_property(img::SVG, property::StrokePrimitive) if property.color.alpha != 1.0 @printf(img.out, " stroke=\"%s\" stroke-opacity=\"%0.3f\"", svg_fmt_color(color(property.color)), property.color.alpha) else @printf(img.out, " stroke=\"%s\"", svg_fmt_color(color(property.color))) end end function print_property(img::SVG, property::FillPrimitive) if property.color.alpha != 1.0 @printf(img.out, " fill=\"%s\" fill-opacity=\"%0.3f\"", svg_fmt_color(color(property.color)), property.color.alpha) else @printf(img.out, " fill=\"%s\"", svg_fmt_color(color(property.color))) end end function print_property(img::SVG, property::StrokeDashPrimitive) if isempty(property.value) print(img.out, " stroke-dasharray=\"none\"") else print(img.out, " stroke-dasharray=\"") svg_print_float(img.out, property.value[1].value) for i in 2:length(property.value) print(img.out, ',') svg_print_float(img.out, property.value[i].value) end print(img.out, '"') end end # Format a line-cap specifier into the attribute string that SVG expects. svg_fmt_linecap(::LineCapButt) = "butt" svg_fmt_linecap(::LineCapSquare) = "square" svg_fmt_linecap(::LineCapRound) = "round" print_property(img::SVG, property::StrokeLineCapPrimitive) = @printf(img.out, " stroke-linecap=\"%s\"", svg_fmt_linecap(property.value)) # Format a line-join specifier into the attribute string that SVG expects. svg_fmt_linejoin(::LineJoinMiter) = "miter" svg_fmt_linejoin(::LineJoinRound) = "round" svg_fmt_linejoin(::LineJoinBevel) = "bevel" print_property(img::SVG, property::StrokeLineJoinPrimitive) = @printf(img.out, " stroke-linejoin=\"%s\"", svg_fmt_linejoin(property.value)) function print_property(img::SVG, property::LineWidthPrimitive) print(img.out, " stroke-width=\"") svg_print_float(img.out, property.value.value) print(img.out, '"') end function print_property(img::SVG, property::FillOpacityPrimitive) print(img.out, " fill-opacity=\"") svg_print_float(img.out, property.value) print(img.out, '"') end function print_property(img::SVG, property::StrokeOpacityPrimitive) print(img.out, " stroke-opacity=\"") svg_print_float(img.out, property.value) print(img.out, '"') end print_property(img::SVG, property::VisiblePrimitive) = @printf(img.out, " visibility=\"%s\"", property.value ? "visible" : "hidden") # I may end up applying the same clip path to many forms separately, so I # shouldn't make a new one for each applicaiton. Where should that happen? function print_property(img::SVG, property::ClipPrimitive) url = clippathurl(img, property) @printf(img.out, " clip-path=\"url(#%s)\"", url) end print_property(img::SVG, property::FontPrimitive) = @printf(img.out, " font-family=\"%s\"", escape_string(property.family)) function print_property(img::SVG, property::FontSizePrimitive) print(img.out, " font-size=\"") svg_print_float(img.out, property.value.value) print(img.out, '"') end print_property(img::SVG, property::SVGIDPrimitive) = @printf(img.out, " id=\"%s\"", escape_string(property.value)) print_property(img::SVG, property::SVGClassPrimitive) = @printf(img.out, " class=\"%s\"", escape_string(property.value)) print_property(img::SVG, property::SVGAttributePrimitive) = @printf(img.out, " %s=\"%s\"", property.attribute, escape_string(property.value)) function print_property(img::SVG, property::JSIncludePrimitive) push!(img.jsheader, property.value) if property.jsmodule != nothing push!(img.jsmodules, property.jsmodule) end end function print_property(img::SVG, property::JSCallPrimitive) @assert img.has_current_id push!(img.scripts, @sprintf("fig.select(\"#%s\")\n .%s;", img.current_id, property.code)) end # Print the property at the given index in each vector property function print_vector_properties(img::SVG, idx::Int, suppress_fill::Bool=false) if haskey(img.vector_properties, JSCall) if haskey(img.vector_properties, SVGID) img.current_id = img.vector_properties[SVGID].primitives[idx].value else img.current_id = genid(img) print_property(img, SVGIDPrimitive(img.current_id)) end img.has_current_id = true end has_stroke_opacity = haskey(img.vector_properties, StrokeOpacity) has_fill_opacity = haskey(img.vector_properties, FillOpacity) for (propertytype, property) in img.vector_properties if property === nothing || (propertytype == Fill && suppress_fill) continue end idx > length(property.primitives) && error("Vector form and vector property differ in length. Can't distribute.") # let the opacity primitives clobber the alpha value in fill and stroke if propertytype == Fill && has_fill_opacity print_property(img, FillPrimitive(RGBA{Float64}(color(property.primitives[idx].color), 1.0))) elseif propertytype == Stroke && has_stroke_opacity print_property(img, StrokePrimitive(RGBA{Float64}(color(property.primitives[idx].color), 1.0))) else print_property(img, property.primitives[idx]) end end img.has_current_id = false end function print_property(img::SVG, property::ArrowPrimitive) print(img.out, " marker-end=\"url(#arrow)\"") end # Form Drawing # ------------ function draw(img::SVG, form::Form{T}) where T for i in 1:length(form.primitives) draw(img, form.primitives[i], i) end end function draw(img::SVG, prim::RectanglePrimitive, idx::Int) # SVG will hide rectangles with zero height or width. We'd prefer to have # zero width/height rectangles stroked, so this is a work-around. width = max(prim.width, 0.01mm) height = max(prim.height, 0.01mm) x0 = prim.corner[1] + width/2 y0 = prim.corner[2] + height/2 translated_path = [(-width/2,-height/2), ( width/2,-height/2), ( width/2, height/2), (-width/2, height/2)] indent(img) img.indentation += 1 print(img.out, "\n") indent(img) print(img.out, "\n") img.indentation -= 1 indent(img) print(img.out, "\n") end function draw(img::SVG, prim::PolygonPrimitive, idx::Int) n = length(prim.points) n <= 1 && return x0, y0 = prim.points[1][1], prim.points[1][2] for p in prim.points[2:end] x0 += p[1] y0 += p[2] end x0, y0 = x0/n, y0/n translated_path = [(p[1]-x0,p[2]-y0) for p in prim.points] indent(img) img.indentation += 1 print(img.out, "\n") indent(img) write(img.out, "\n") img.indentation -= 1 indent(img) print(img.out, "\n") end function draw(img::SVG, prim::ComplexPolygonPrimitive, idx::Int) Compose.write(img.out, "\n") end function draw(img::SVG, prim::CirclePrimitive, idx::Int) indent(img) img.indentation += 1 print(img.out, "\n") indent(img) print(img.out, "\n") img.indentation -= 1 indent(img) print(img.out, "\n") end function draw(img::SVG, prim::EllipsePrimitive, idx::Int) cx = prim.center[1].value cy = prim.center[2].value rx = sqrt((prim.x_point[1].value - cx)^2 + (prim.x_point[2].value - cy)^2) ry = sqrt((prim.y_point[1].value - cx)^2 + (prim.y_point[2].value - cy)^2) theta = rad2deg(atan(prim.x_point[2].value - cy, prim.x_point[1].value - cx)) all(isfinite,[cx, cy, rx, ry, theta]) || return indent(img) img.indentation += 1 print(img.out, "\n") indent(img) print(img.out, " 1e-4 print(img.out, " transform=\"rotate(") svg_print_float(img.out, theta) print(img.out, ")\"") end print(img.out, " class=\"primitive\"") print(img.out, "/>\n") img.indentation -= 1 indent(img) print(img.out, "\n") end function draw(img::SVG, prim::LinePrimitive, idx::Int) length(prim.points)<=1 && return i = [isfinite(p[1].value) && isfinite(p[2].value) for p in prim.points] any(i) || return x0, y0 = mean(prim.points[i]) translated_path = prim.points .- [(x0, y0)] indent(img) img.indentation += 1 print(img.out, "\n") indent(img) print(img.out, "\n") img.indentation -= 1 indent(img) print(img.out, "\n") end function draw(img::SVG, prim::TextPrimitive, idx::Int) indent(img) img.indentation += 1 print(img.out, "\n") indent(img) print(img.out, "\n") img.indentation += 1 indent(img) print(img.out, " 1e-4 || sum(abs.(prim.offset)) > 1e-4mm print(img.out, " transform=\"") if abs(prim.rot.theta) > 1e-4 print(img.out, "rotate(") svg_print_float(img.out, rad2deg(prim.rot.theta)) print(img.out, ",") svg_print_float(img.out, prim.rot.offset[1].value-prim.position[1].value) print(img.out, ", ") svg_print_float(img.out, prim.rot.offset[2].value-prim.position[2].value) print(img.out, ")") end if sum(abs.(prim.offset)) > 1e-4mm print(img.out, "translate(") svg_print_float(img.out, prim.offset[1].value) print(img.out, ",") svg_print_float(img.out, prim.offset[2].value) print(img.out, ")") end print(img.out, "\"") end @printf(img.out, ">%s\n", pango_to_svg(prim.value)) img.indentation -= 1 indent(img) print(img.out, "\n") img.indentation -= 1 indent(img) print(img.out, "\n") end function draw(img::SVG, prim::CurvePrimitive, idx::Int) x0, y0 = prim.anchor0[1], prim.anchor0[2] x0, y0 += prim.ctrl0[1], prim.ctrl0[2] x0, y0 += prim.ctrl1[1], prim.ctrl1[2] x0, y0 += prim.anchor1[1], prim.anchor1[2] x0, y0 = x0/4, y0/4 translated_anchor0 = [prim.anchor0...] - [x0,y0] translated_ctrl0 = [prim.ctrl0...] - [x0,y0] translated_ctrl1 = [prim.ctrl1...] - [x0,y0] translated_anchor1 = [prim.anchor1...] - [x0,y0] indent(img) img.indentation += 1 print(img.out, "\n") indent(img) print(img.out, "\n") img.indentation -= 1 indent(img) print(img.out, "\n") end function draw(img::SVG, prim::BitmapPrimitive, idx::Int) indent(img) img.indentation += 1 print(img.out, "\n") indent(img) print(img.out, "\n") img.indentation -= 1 indent(img) print(img.out, "\n") end # FormBatch Drawing # ----------------- function draw(img::SVG, batch::FormBatch) id = genid(img) push!(img.batches, (batch.primitive, id)) for i in 1:length(batch.offsets) indent(img) print(img.out, "\n") end end # Applying properties # ------------------- # Return a URL corresponding to a ClipPrimitive clippathurl(img::SVG, property::ClipPrimitive) = get!(() -> genid(img), img.clippaths, property) function push_property_frame(img::SVG, properties::Vector{Property}) isempty(properties) && return svgalphatest(properties) frame = SVGPropertyFrame() applied_properties = Set{Type}() scalar_properties = Array{Property}(undef, 0) isplotpanel = false for property in properties if !isrepeatable(property) && (typeof(property) in applied_properties) continue elseif isscalar(property) && isa(property, Clip) # clip-path needs to be in it's own group. Otherwise it can cause # problems if we apply a transform to the group. indent(img) img.indentation += 1 write(img.out, "\n") frame.has_scalar_clip = true elseif isscalar(property) push!(scalar_properties, property) push!(applied_properties, typeof(property)) frame.has_scalar_properties = true img.vector_properties[typeof(property)] = nothing else frame.vector_properties[typeof(property)] = property img.vector_properties[typeof(property)] = property end isplotpanel |= typeof(property)==SVGClass && length(property.primitives)==1 && property.primitives[1].value=="plotpanel" end push!(img.property_stack, frame) isempty(scalar_properties) && return id_needed = any([isa(property, JSCall) for property in scalar_properties]) for property in scalar_properties if isa(property, SVGID) img.current_id = property.primitives[1].value img.has_current_id = true end end if !img.has_current_id img.current_id = genid(img) push!(scalar_properties, svgid(img.current_id)) img.has_current_id = true end indent(img) write(img.out, "\n"); img.has_current_id = false img.indentation += 1 if isplotpanel indent(img) write(img.out, "\n") boundingBox = string(img.panelcoords[1].x0[1], ' ', img.panelcoords[1].x0[2], ' ', img.panelcoords[1].a[1], ' ', img.panelcoords[1].a[2]) unitBox = string(img.panelcoords[2].x0, ' ', img.panelcoords[2].y0, ' ', img.panelcoords[2].width, ' ', img.panelcoords[2].height) indent(img) write(img.out, " \n") indent(img) write(img.out, " \n") indent(img) write(img.out, "\n") end end function pop_property_frame(img::SVG) @assert !isempty(img.property_stack) frame = pop!(img.property_stack) if frame.has_scalar_properties img.indentation -= 1 indent(img) write(img.out, "") frame.has_link && write(img.out, "") frame.has_mask && write(img.out, "") write(img.out, "\n") end if frame.has_scalar_clip img.indentation -= 1 indent(img) write(img.out, "\n") end for (propertytype, property) in frame.vector_properties img.vector_properties[propertytype] = nothing for i in length(img.property_stack):-1:1 if haskey(img.property_stack[i].vector_properties, propertytype) img.vector_properties[propertytype] = img.property_stack[i].vector_properties[propertytype] end end end end function Compose.draw(img::SVG, prim::ArcPrimitive, idx::Int) xc = prim.center[1].value yc = prim.center[2].value rx = ry = prim.radius.value x1 = rx*cos(prim.angle1) y1 = ry*sin(prim.angle1) x2 = rx*cos(prim.angle2) y2 = ry*sin(prim.angle2) dθ = prim.angle2 - prim.angle1 dθ += 2π*(dθ<0) indent(img) img.indentation += 1 print(img.out, "\n") indent(img) print(img.out, "π ? 1 : 0,' ',1,' ') svg_print_float(img.out, x2) print(img.out, ",") svg_print_float(img.out, y2) prim.sector && print(img.out, "L0,0") print(img.out, '"') print(img.out, " class=\"primitive\"") print(img.out, "/>\n") img.indentation -= 1 indent(img) print(img.out, "\n") end # Currently for svg, you can't use a transparent fill(color) and fillopacity() together, # so throw a warning if a user tries to do that. function svgalphatest(properties::Vector{Property}) has_fill_opacity = any(isa.(properties, Property{FillOpacityPrimitive})) !has_fill_opacity && return is_fill = isa.(properties, Property{FillPrimitive}) !any(is_fill) && return fillproperties = properties[is_fill][1] has_alpha = any([alpha(x.color) for x in fillproperties.primitives].<1.0) has_alpha && @warn "For svg transparent colors, use either e.g. fill(RGBA(r,g,b,a)) or fillopacity(a), but not both." end function draw(img::SVG, prim::BezierPolygonPrimitive, idx::Int) points = [prim.anchor; reduce(vcat, prim.sides)] x0 = sum(first.(points))/length(points) y0 = sum(last.(points))/length(points) sv = Vector{Vec}[] for side in prim.sides s = collect(Tuple{Measure, Measure}, zip(first.(side).-x0, last.(side).-y0)) push!(sv, s) end anchor0 = (prim.anchor[1]-x0, prim.anchor[2]-y0) P = Dict(1=>" L", 2=>" Q", 3=>" C") indent(img) img.indentation += 1 print(img.out, "\n") indent(img) print(img.out, "\n") img.indentation -= 1 indent(img) print(img.out, "\n") end ================================================ FILE: src/table-jump.jl ================================================ using JuMP is_approx_integer(x::Float64) = abs(x - round(x)) < 1e-8 function realize(tbl::Table, drawctx::ParentDrawContext) model = Model() m, n = size(tbl.children) abswidth = drawctx.box.a[1].value absheight = drawctx.box.a[2].value c_indexes = Tuple{Int, Int, Int}[] idx_cs = Dict{(Tuple{Int, Int}), Vector{Int}}() for i in 1:m, j in 1:n if length(tbl.children[i, j]) > 1 for k in 1:length(tbl.children[i, j]) push!(c_indexes, (i, j, k)) if haskey(idx_cs, (i, j)) push!(idx_cs[(i,j)], length(c_indexes)) else idx_cs[(i,j)] = [length(c_indexes)] end end end end penalties = Array{Float64}(undef, length(c_indexes)) for (l, (i, j, k)) in enumerate(c_indexes) penalties[l] = tbl.children[i, j][k].penalty end # 0-1 configuration variables for every cell with multiple configurations @defVar(model, c[1:length(c_indexes)], Bin) # width for every column @defVar(model, 0 <= w[1:n] <= abswidth) # height for every row @defVar(model, 0 <= h[1:m] <= absheight) # maximize the "size" of the focused cells @setObjective(model, Max, sum{w[j], j=tbl.x_focus} + sum{h[i], i=tbl.y_focus} - sum{penalties[i] * c[i], i=1:length(c_indexes)}) # optional proportionality constraints if tbl.x_prop != nothing k1 = 1 while k1 < length(tbl.x_focus) && isnan(tbl.x_prop[k1]) k1 += 1 end for k in 2:length(tbl.x_focus) isnan(tbl.x_prop[k]) && continue j1 = tbl.x_focus[k1] jk = tbl.x_focus[k] @addConstraint(model, w[j1] / tbl.x_prop[k1] == w[jk] / tbl.x_prop[k]) end end if tbl.y_prop != nothing k1 = 1 while k1 < length(tbl.y_focus) && isnan(tbl.y_prop[k1]) k1 += 1 end for k in 2:length(tbl.y_focus) isnan(tbl.y_prop[k]) && continue i1 = tbl.y_focus[k1] ik = tbl.y_focus[k] @addConstraint(model, h[i1] / tbl.y_prop[k1] == h[ik] / tbl.y_prop[k]) end end # fixed configuration constraints: constrain a set of cells # to have tho same configuration. for fixed_config in tbl.fixed_configs isempty(fixed_config) && continue (i0, j0) = fixed_config[1] config_count = length(tbl.children[i0, j0]) for (i, j) in fixed_config[2:end], k in 1:config_count !haskey(idx_cs, (i0, j0)) && !haskey(idx_cs, (i, j)) && continue idx_a = idx_cs[i0, j0][k] idx_b = idx_cs[(i,j)][k] @addConstraint(model, c[idx_a] == c[idx_b]) end end # configurations are mutually exclusive for cgroup in groupby(l -> (c_indexes[l][1], c_indexes[l][2]), 1:length(c_indexes)) @addConstraint(model, sum{c[l], l=cgroup} == 1) end # minimum cell size constraints for cells with multiple configurations for (l, (i, j, k)) in enumerate(c_indexes) minw = minwidth(tbl.children[i, j][k]) minh = minheight(tbl.children[i, j][k]) minw == nothing || @addConstraint(model, w[j] >= minw * c[l]) minh == nothing || @addConstraint(model, h[i] >= minh * c[l]) end # minimum cell size constraint for fixed cells for i in 1:m, j in 1:n if length(tbl.children[i, j]) == 1 minw = minwidth(tbl.children[i, j][1]) minh = minheight(tbl.children[i, j][1]) minw == nothing || @addConstraint(model, w[j] >= minw) minh == nothing || @addConstraint(model, h[i] >= minh) end end # widths and heights must add up @addConstraint(model, sum{w[i], i=1:n} == abswidth) @addConstraint(model, sum{h[i], i=1:m} == absheight) status = solve(model,suppress_warnings=true) w_solution = getValue(w)[:] h_solution = getValue(h)[:] c_solution = getValue(c) if status == :Infeasible || !all([is_approx_integer(c_solution[l]) for l in 1:length(c_indexes)]) #println(STDERR, "JuMP: Infeasible") # The brute force solver is better able to select between various # non-feasible solutions. So we let it have a go. return realize_brute_force(tbl, drawctx) end # Set positions and sizes of children root = context(units=tbl.units, order=tbl.order) x_solution = cumsum([w_solution[j] for j in 1:n]) .- w_solution y_solution = cumsum([h_solution[i] for i in 1:m]) .- h_solution tbl.aspect_ratio == nothing || force_aspect_ratio!(tbl, x_solution, y_solution, w_solution, h_solution) # set child positions according to layout solution feasible_eps = 1e-4 feasible = true for i in 1:m, j in 1:n if length(tbl.children[i, j]) == 1 ctx = copy(tbl.children[i, j][1]) feasible == feasible && issatisfied(ctx, w_solution[j], h_solution[i]) ctx.box = BoundingBox( x_solution[j]*mm, y_solution[i]*mm, w_solution[j]*mm, h_solution[i]*mm) compose!(root, ctx) end end for (l, (i, j, k)) in enumerate(c_indexes) if round(c_solution[l]) == 1 ctx = copy(tbl.children[i, j][k]) feasible == feasible && issatisfied(ctx, w_solution[j], h_solution[i]) ctx.box = BoundingBox( x_solution[j]*mm, y_solution[i]*mm, w_solution[j]*mm, h_solution[i]*mm) compose!(root, ctx) end end feasible || warn("Graphic may not be drawn correctly at the given size.") return root end ================================================ FILE: src/table.jl ================================================ # A special kind of container promise that performs table layout optimization. mutable struct Table <: ContainerPromise # Direct children must be Contexts, and not just Containers. If # children[i,j] has a vector with multiple children it indicates multiple # possible layouts for that cell in the table. children::Matrix{Vector{Context}} # In the formulation of the table layout problem used here, we are trying # find a feasible solution in which the width + height of a particular # group of cells in the table is maximized. x_focus::UnitRange{Int} y_focus::UnitRange{Int} # If non-nothing, constrain the focused cells to have a proportional # relationship. x_prop::Union{Vector{Float64}, Nothing} y_prop::Union{Vector{Float64}, Nothing} # If non-nothing, constrain the focused cells to have a fixed aspect ratio. aspect_ratio::Union{Float64, Nothing} # fixed configuration fixed_configs::Vector # Coordinate system used for children units::Union{UnitBox, Nothing} # Z-order of this context relative to its siblings. order::Int # Ignore this context and everything under it if we are # not drawing to the javascript backend. withjs::Bool # Ignore this context if we are drawing to the SVGJS backend. withoutjs::Bool end function Table(m::Integer, n::Integer, y_focus::UnitRange{Int}, x_focus::UnitRange{Int}; y_prop=nothing, x_prop=nothing, aspect_ratio=nothing, units=nothing, order=0, withjs=false, withoutjs=false, fixed_configs=Any[]) if x_prop != nothing @assert length(x_prop) == length(x_focus) x_prop ./= sum(filter(x -> !isnan(x), x_prop)) end if y_prop != nothing @assert length(y_prop) == length(y_focus) y_prop ./= sum(filter(x -> !isnan(x), y_prop)) end tbl = Table(Array{Vector{Context}}(undef, (m, n)), x_focus, y_focus, x_prop, y_prop, aspect_ratio, fixed_configs, units, order, withjs, withoutjs) for i in 1:m, j in 1:n tbl.children[i, j] = Array{Context}(undef, 0) end return tbl end const table = Table getindex(t::Table, i::Integer, j::Integer) = t.children[i, j] setindex!(t::Table, child, i::Integer, j::Integer) = t.children[i, j] = child size(t::Table, i::Integer) = size(t.children, i) size(t::Table) = size(t.children) # Adjust a table solution so that the aspect ratio matches tbl.aspect_ratio # # Returns: # true if the solution is feasibly, false if not # # Modifies: # x_solution, y_solution, w_solution, h_solution # function force_aspect_ratio!(tbl::Table, x_solution::Vector, y_solution::Vector, w_solution::Vector, h_solution::Vector) w0 = sum(w_solution[tbl.x_focus]) h0 = sum(h_solution[tbl.y_focus]) adj = (w0 / h0) / tbl.aspect_ratio # we can't expand either dimension (since it's presumably of maximum size) # so shrink one or the other. if adj > 1.0 w = w0 / adj delta = w0 - w x_solution[1:tbl.x_focus.stop] .+= delta/2 x_solution[tbl.x_focus.stop+1:end] .-= delta/2 w_solution[tbl.x_focus] ./= adj else w = w0 h = h0 * adj delta = h0 - h y_solution[1:tbl.y_focus.stop] .+= delta/2 y_solution[tbl.y_focus.stop+1:end] .-= delta/2 h_solution[tbl.y_focus] *= adj end end # Return true if the minwidth and minheight constraints on ctx are satisfied # by the given width/height function issatisfied(ctx::Context, width, height) eps = 1e-4 return (minwidth(ctx) == nothing || width + eps >= minwidth(ctx)) && (minheight(ctx) == nothing || height + eps >= minheight(ctx)) end # Solve the table layout using a brute force approach, when a MILP isn't # available. function realize_brute_force(tbl::Table, drawctx::ParentDrawContext) m, n = size(tbl.children) maxobjective = 0.0 optimal_choice = nothing feasible = false # is the current optimal_choice feasible # if the current solution is infeasible, we try to minimize badness, # which is basically "size needed" - "size available". minbadness = Inf focused_col_widths = Array{Float64}(undef, length(tbl.x_focus)) focused_row_heights = Array{Float64}(undef, length(tbl.y_focus)) # minimum sizes for each column and row minrowheights = Array{Float64}(undef, m) mincolwidths = Array{Float64}(undef, n) # convert tbl.fixed_configs to linear indexing fixed_configs = Any[ Set([(j-1)*m + i for (i, j) in fixed_config]) for fixed_config in tbl.fixed_configs ] # build equilavence classes of configurations basen on fixed_configs constrained_cells = Set() num_choices = [length(child) for child in tbl.children] num_group_choices = Any[] for fixed_config in fixed_configs push!(num_group_choices, num_choices[first(fixed_config)]) for idx in fixed_config push!(constrained_cells, idx) end end for (idx, child) in enumerate(tbl.children) if length(child) > 1 if !in(idx, constrained_cells) push!(num_group_choices, length(child)) push!(fixed_configs, Set([idx])) end end end # compute the optimal column widths/row heights for fixed choice and # pre-computed minrowheight/mincolwidths. function update_focused_col_widths!(focused_col_widths) minwidth = sum(mincolwidths) total_focus_width = drawctx.box.a[1].value - minwidth + sum(mincolwidths[tbl.x_focus]) if tbl.x_prop != nothing for k in 1:length(tbl.x_focus) if isnan(tbl.x_prop[k]) total_focus_width -= mincolwidths[tbl.x_focus[k]] end end for k in 1:length(tbl.x_focus) if !isnan(tbl.x_prop[k]) focused_col_widths[k] = tbl.x_prop[k] * total_focus_width else focused_col_widths[k] = mincolwidths[tbl.x_focus[k]] end end else extra_width = total_focus_width - sum(mincolwidths[tbl.x_focus]) for k in 1:length(tbl.x_focus) focused_col_widths[k] = mincolwidths[tbl.x_focus[k]] + extra_width / length(tbl.x_focus) end end end function update_focused_row_heights!(focused_row_heights) total_focus_height = drawctx.box.a[2].value - sum(minrowheights) + sum(minrowheights[tbl.y_focus]) if tbl.y_prop != nothing for k in 1:length(tbl.y_focus) if isnan(tbl.y_prop[k]) total_focus_height -= minrowheights(tbl.y_focus[k]) end end for k in 1:length(tbl.y_focus) if !isnan(tbl.y_prop[k]) focused_row_heights[k] = tbl.y_prop[k] * total_focus_height else focused_row_heights[k] = minrowheights[tbl.y_focus[k]] end end else extra_height = total_focus_height - sum(minrowheights[tbl.y_focus]) for k in 1:length(tbl.y_focus) focused_row_heights[k] = minrowheights[tbl.y_focus[k]] + extra_height / length(tbl.y_focus) end end end # for a given configuration, compute the minimum width for every column and # minimum height for every row. Return the penalty for choice. function update_mincolrow_sizes!(choice, minrowheights, mincolwidths) fill!(minrowheights, -Inf) fill!(mincolwidths, -Inf) penalty = 0.0 for i in 1:m, j in 1:n isempty(tbl.children[i, j]) && continue choice_ij = choice[(j-1)*m + i] child = tbl.children[i, j][(choice_ij == 0 ? 1 : choice_ij)] penalty += child.penalty mw, mh = minwidth(child), minheight(child) if mw != nothing && mw > mincolwidths[j] mincolwidths[j] = mw end if mh != nothing && mh > minrowheights[i] minrowheights[i] = mh end end minrowheights[isfinite.(minrowheights) .== false] .= 0.0 mincolwidths[isfinite.(mincolwidths) .== false] .= 0.0 return penalty end it_count = 0 group_choices = [l == 0 ? (0:0) : (1:l) for l in num_group_choices] choice = zeros(Int, m * n) optimal_choice = nothing if !isempty(group_choices) for group_choice in Iterators.product(group_choices...) it_count += 1 for (l, k) in enumerate(group_choice) for p in fixed_configs[l] choice[p] = k end end penalty = update_mincolrow_sizes!(choice, minrowheights, mincolwidths) minheightval = sum(minrowheights) minwidthval = sum(mincolwidths) update_focused_col_widths!(focused_col_widths) update_focused_row_heights!(focused_row_heights) objective = sum(focused_col_widths) + sum(focused_row_heights) - penalty # feasible? if minwidthval < drawctx.box.a[1].value && minheightval < drawctx.box.a[2].value && all(focused_col_widths .>= mincolwidths[tbl.x_focus]) && all(focused_row_heights .>= minrowheights[tbl.y_focus]) if objective > maxobjective || !feasible maxobjective = objective minbadness = 0.0 optimal_choice = copy(choice) end feasible = true else badness = max(minwidthval - drawctx.box.a[1].value, 0.0) + max(minheightval - drawctx.box.a[2].value, 0.0) if badness < minbadness && !feasible minbadness = badness optimal_choice = copy(choice) end end if optimal_choice === nothing optimal_choice = copy(choice) end end end if optimal_choice === nothing optimal_choice = zeros(Int, m * n) end update_mincolrow_sizes!(optimal_choice, minrowheights, mincolwidths) update_focused_col_widths!(focused_col_widths) update_focused_row_heights!(focused_row_heights) w_solution = mincolwidths h_solution = minrowheights for k in 1:length(tbl.x_focus) w_solution[tbl.x_focus[k]] = focused_col_widths[k] end for k in 1:length(tbl.y_focus) h_solution[tbl.y_focus[k]] = focused_row_heights[k] end x_solution = cumsum(mincolwidths) .- w_solution y_solution = cumsum(minrowheights) .- h_solution if tbl.aspect_ratio != nothing force_aspect_ratio!(tbl, x_solution, y_solution, w_solution, h_solution) end root = context(units=tbl.units, order=tbl.order) feasible = true for i in 1:m, j in 1:n if isempty(tbl.children[i, j]) continue elseif length(tbl.children[i, j]) == 1 ctx = copy(tbl.children[i, j][1]) elseif length(tbl.children[i, j]) > 1 idx = optimal_choice[(j-1)*m + i] ctx = copy(tbl.children[i, j][idx]) end feasible == feasible && issatisfied(ctx, w_solution[j], h_solution[i]) ctx.box = BoundingBox( x_solution[j]*mm, y_solution[i]*mm, w_solution[j]*mm, h_solution[i]*mm) compose!(root, ctx) end feasible || warn("Graphic may not be drawn correctly at the given size.") return root end # TODO: Enable this when we have a mechanism for making it optional #if isinstalled("JuMP") && #(isinstalled("GLPKMathProgInterface") || #isinstalled("Cbc")) #include("table-jump.jl") #else realize(tbl::Table, drawctx::ParentDrawContext) = realize_brute_force(tbl, drawctx) #end function show(io::IO, t::Table) if get(io, :compact, false) println(io,"$(size(t.children,1))x$(size(t.children,2)) Table:") for i = 1:size(t.children,1) print(io, " ") first = true for j = 1:size(t.children,2) first || print(io, ",") first = false show(io, t.children[i,j]) end println(io) end else invoke(show, Tuple{IO, Any}, io, t) end end ================================================ FILE: test/.gitignore ================================================ /*.pdf ================================================ FILE: test/Project.toml ================================================ [deps] Cairo = "159f3aea-2a34-519c-b102-8c37f9878175" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615" Fontconfig = "186bb1d3-e1f7-5a2c-a377-96d770f13627" Measures = "442fdcdd-2543-5da2-b0f3-8c86c306513e" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] Cairo = "0.7, 0.8, 1.0" EzXML = "0.8, 0.9, 1.0" Fontconfig = "0.3, 0.4" ================================================ FILE: test/examples/arc_sector.jl ================================================ using Compose import Cairo, Fontconfig imgs = [SVG("arc_sector.svg", 7cm, 7cm), PDF("arc_sector.pdf", 7cm, 7cm)] a = range(-0.5π, stop=1.5π, length=13) colv = repeat(["white","black"], outer=6) img = compose(context(), (context(), sector([0.5], [0.5], [0.4], a[1:12], a[2:13]), fill(colv)), (context(order=2), arc([0.5], [0.5], [0.2], [0, π], [π,0]), fill(["black","white"]), stroke(["white","black"]), linewidth(6pt)) ) draw.(imgs, [img]) ================================================ FILE: test/examples/arrow.jl ================================================ using Compose import Cairo, Fontconfig imgs = [SVG("arrow.svg", 5cm, 5cm), PDF("arrow.pdf", 5cm, 5cm)] X = [0.047 0.87 0.95 0.93; 0.22 0.01 0.21 0.7; 0.86 0.85 0.95 0.21] point_array = [[(x1,y1), (x2,y2)] for (x1,y1,x2,y2) in zip(X[:,1],X[:,2],X[:,3],X[:,4])] img = compose(context(), (context(), line(point_array), stroke(["red","green","deepskyblue"]), arrow()) ) draw.(imgs, [img]) ================================================ FILE: test/examples/bezigon.jl ================================================ using Compose import Cairo, Fontconfig set_default_graphic_size(6.6inch, 3.3inch) # See Picasso's "dog" sketch dog = [[(183, 268), (186, 256), (189, 244)], [(290, 244), (300, 230), (339, 245)], [(350,290), (360, 300), (355, 210)], [(370, 207), (380,196), (375, 193)], [(310, 220), (190, 220), (164, 205)], [(135, 194), (135, 265), (153, 275)], [(168, 275), (170, 180), (150, 190)], [(122, 214), (142, 204), (85, 240)], [(100, 247), (125, 233), (140, 238)]] ub = UnitBox(0,0, 500,500) p = compose(context(), stroke("black"), fillopacity(0.1), (context(0,0, 0.5,1, units=ub, rotation=Rotation(-π/8)), bezigon((180, 280), dog)), (context(0.5,0, 0.5,1, units=ub, rotation=Rotation(π/8)), bezigon([(180, 280)], [dog])) ) imgs = [SVG("bezigon.svg"), PNG("bezigon.png")] draw.(imgs, [p]) ================================================ FILE: test/examples/dashedlines.jl ================================================ # Draw lines with various dash styles. using Compose, Colors import Cairo, Fontconfig function draw_lines(dash_patterns) if length(dash_patterns) == 0 context() else compose(compose(context(), line([(0, 0), (1, 0)]), strokedash(dash_patterns[1])), compose(context(0, 0.1), draw_lines(dash_patterns[2:end]))) end end patterns = Array[ [5mm, 5mm], [5mm, 10mm], [10mm, 5mm], [5mm, 1mm], [1mm, 5mm], [0.9mm], [15mm, 10mm, 5mm], [15mm, 10mm, 5mm, 10mm], [15mm, 10mm, 5mm, 10mm, 15mm], [5mm, 5mm, 1mm, 5mm]] c = draw_lines(patterns) imgs = [PDF("dash.pdf", 4inch, 4(sqrt(3)/2)inch), PGF("dash.pgf", 4inch, 4(sqrt(3)/2)inch), PGF("dash_texfonts.pgf", 4inch, 4(sqrt(3)/2)inch; texfonts=true)] for img = imgs compose(c, stroke("black"), linewidth(1mm)) |> img end ================================================ FILE: test/examples/forms_and_nans.jl ================================================ using Compose import Cairo, Fontconfig set_default_graphic_size(14cm, 10cm) y = [0.26, 0.5, missing, 0.4, NaN, 0.48, 0.58, 0.83] p1 = collect(Tuple, zip(1:8, y)) p2 = collect(Tuple, zip(1:9, vcat(NaN, y))) img = compose(context(units=UnitBox(0, 0, 10, 1)), (context(), line([p1]), stroke("black")), (context(), line([p2]), stroke("red")) ) imgs = [SVG("forms_and_nans.svg"), PDF("forms_and_nans.pdf")] draw.(imgs, [img]) ================================================ FILE: test/examples/golden_rect.jl ================================================ using Compose, Colors using Base.MathConstants function golden_rect(n::Int) poly_points = [(0, 0), (1, 0), (1, 1), (0, 1)] if n == 0 return context() end c = compose(context(), polygon(poly_points), fill(LCHab(90, 80, 70-20n)), stroke("black")) compose(c, (context(0,0,1/φ,1/φ, rotation=Rotation(-π/2,1,0)), golden_rect(n-1))) end draw(SVG("golden_rect.svg", φ*3inch, 3inch), compose(golden_rect(8), linewidth(0.2mm))) ================================================ FILE: test/examples/linecaps.jl ================================================ # Draw lines with various dash styles. using Compose, Colors import Cairo, Fontconfig function draw_lines(caps) if length(caps) == 0 context() else compose(compose(context(), line([(5mm, 5mm), (15mm, 5mm)]), strokelinecap(caps[1])), compose(context(0, 5mm), draw_lines(caps[2:end]))) end end caps = [LineCapButt(), LineCapSquare(), LineCapRound()] c = draw_lines(caps) imgs = [SVG("linecaps.svg", 2cm, 3cm), PDF("linecaps.pdf", 2cm, 3cm)] for img = imgs draw(img, compose(c, stroke("black"), linewidth(2mm))) end ================================================ FILE: test/examples/linejoins.jl ================================================ # Draw lines with various dash styles. using Compose, Colors import Cairo, Fontconfig function draw_lines(joins) if length(joins) == 0 context() else compose(compose(context(), line([(5mm, 5mm), (10mm, 10mm), (15mm, 5mm)]), strokelinejoin(joins[1])), compose(context(0, 5mm), draw_lines(joins[2:end]))) end end joins = [Compose.LineJoinRound(), Compose.LineJoinMiter(), Compose.LineJoinBevel()] c = draw_lines(joins) imgs = [SVG("linejoins.svg", 2cm, 3cm), PDF("linejoins.pdf", 2cm, 3cm), SVGJS("linejoins.js", 2cm, 3cm)] for img = imgs draw(img, compose(c, stroke("black"), linewidth(2mm), fill(nothing))) end ================================================ FILE: test/examples/polygon_forms.jl ================================================ using Compose compose(context(), ngon(0.15, 0.15, 0.08, 5), star(0.35, 0.15, 0.08, 5, 0.3), xgon(0.55, 0.15, 0.08, 5, 0.3) ) |> SVG("polygon_forms.svg", 3inch, 3inch) ================================================ FILE: test/examples/primitives.jl ================================================ using Compose rawimg = read(joinpath(@__DIR__,"smiley.png")); compose(context(), rectangle(0.1,0.1,0.1,0.1), circle(0.3,0.15,0.05), polygon([(0.45,0.1),(0.4,0.2),(0.5,0.2)]), ellipse(0.65,0.15,0.1,0.05), (context(), stroke("black"), line([(0.1,0.3),(0.2,0.3)]), curve((0.25,0.35),(0.25,0.25),(0.35,0.25),(0.35,0.35))), bitmap("image/png",rawimg,0.4,0.25,0.1,0.1), text(0.6,0.3,"hello")) |> SVG("primitives.svg") ================================================ FILE: test/examples/text.jl ================================================ # Draw some text to a PNG image. using Compose lines = "hello and goodbye\nFooSubBarSup\nA Third Line\nFooSubBarSupPooh\nA Fifth Line\nA Sixth Line" c = compose(context(), (context(), text(0px, 250px, lines), fontsize(8pt), fill("tomato")), (context(), text(100px, 250px, lines), fontsize(24pt), fill("bisque"))) draw(SVG("text-fontfallback.svg", 400px, 400px), c) import Cairo, Fontconfig draw(SVG("text-pango.svg", 400px, 400px), c) draw(PNG("text.png", 400px, 400px, dpi=192), c) draw(PDF("text.pdf", 400px, 400px), c) ================================================ FILE: test/examples/transformations.jl ================================================ using Compose import Cairo, Fontconfig imgs = [SVG("transformations.svg", 7cm, 7cm), PDF("transformations.pdf", 7cm, 7cm)] img = compose(context(), (context(0,0,0.5,0.5), xgon(0.2,0.2,0.1,3,0.3), (context(mirror=Mirror(-π/4, 0.4,0.4)), xgon(0.2,0.2,0.1,3,0.3)) ), (context(0.5,0,0.5,0.5), xgon(0.5,0.5,0.2,3,0.3), fill("silver"), (context(shear=Shear(0.8, 0, 0.5,0.5)), xgon(0.5,0.5,0.2,3,0.3), fill(nothing), stroke("black")) ) ) draw.(imgs, [img]) ================================================ FILE: test/examples/unicode.jl ================================================ # Draw some unicodes to an image. # https://github.com/GiovineItalia/Compose.jl/pull/360#issuecomment-539283765 using Compose import Cairo, Fontconfig c = compose(context(), text(0.5, 0.5, "𝑎 𝑏 𝑐 𝑑 𝑒 𝑓", hcenter, vcenter), font("Segoe UI"), fontsize(20pt) ) # Note: the string is composed with \ita \itb etc. imgs = [ PDF("unicode.pdf") SVG("unicode.svg") PNG("unicode.png") ] draw.(imgs, [c]) ================================================ FILE: test/immerse.jl ================================================ # These tests are designed to ensure that Immerse.jl works. If you # need to edit these tests to make them pass, that's fine, but please # submit the corresponding fix to Immerse. using Test using Compose import Cairo import Measures ### The Immerse backend srf = Cairo.CairoImageSurface(10, 10, Cairo.FORMAT_RGB24) ctx = compose(compose(context(), rectangle(0.0w, 0.0h, 0.8w, 0.7h, :rect)), fill("tomato")) be = Compose.ImmerseBackend(srf) draw(be, ctx) @test be.coords[:rect][2] == Compose.UnitBox() ### Finding tagged objects const ContainersWithChildren = Union{Context,Compose.Table} const Iterables = Union{ContainersWithChildren, AbstractArray} iterable(ctx::ContainersWithChildren) = ctx.children iterable(a::AbstractArray) = a function find_tagged(root) handles = Dict{Symbol,Context}() find_tagged!(handles, root) end function find_tagged!(handles, obj::Iterables) for item in iterable(obj) if has_tag(item) handles[item.tag] = obj else find_tagged!(handles, item) end end handles end function find_tagged!(handles, obj::Context) for item in obj.form_children if has_tag(item) handles[item.tag] = obj end end for item in obj.container_children find_tagged!(handles, item) end handles end find_tagged!(handles, obj) = obj has_tag(form::Compose.Form, tag) = form.tag == tag has_tag(form::Compose.Form) = form.tag != Compose.empty_tag has_tag(obj, tag) = false has_tag(obj) = false @test find_tagged(ctx)[:rect] == ctx ### Coordinate computations function absolute_to_data(x, y, transform, unit_box, parent_box) xt, yt = invert_transform(transform, x, y) (unit_box.x0 + unit_box.width *(xt-parent_box.x0[1])/Measures.width(parent_box), unit_box.y0 + unit_box.height*(yt-parent_box.x0[2])/Measures.height(parent_box)) end invert_transform(::Compose.IdentityTransform, x, y) = x, y function invert_transform(t::Compose.MatrixTransform, x, y) @assert t.M[3,1] == t.M[3,2] == 0 xyt = t.M\[x, y, 1.0] xyt[1], xyt[2] end box, units, transform = be.coords[:rect] xd, yd = absolute_to_data(0.5mm, 0.5mm, transform, units, box) @test isapprox(xd,0.1417; atol=0.0001) ================================================ FILE: test/misc.jl ================================================ using Compose, Test, Random, Colors # must be before importing cairo @testset "missing cairo errors" begin ctx = compose(context(), circle(), fill("gold")) @test_throws ErrorException ctx |> PNG("test.png") @test_throws ErrorException ctx |> PDF("test.pdf") @test_throws ErrorException ctx |> PS("test.ps") io = IOBuffer() @test_throws ErrorException show(io, MIME("image/png"), ctx) @test_throws ErrorException show(io, MIME("application/pdf"), ctx) @test_throws ErrorException show(io, MIME("application/ps"), ctx) end import Cairo # showcompact @testset "printing" begin @testset "context" begin io = IOBuffer() tomato_bisque = compose(context(), (context(), circle(), fill(colorant"bisque")), (context(), rectangle(), fill(colorant"tomato"))) # compact printing show(IOContext(io, :compact=>true), tomato_bisque) str = String(take!(io)) @test str == "Context(Context(R,f),Context(C,f))" # full printing show(io, context()) str = String(take!(io)) @test replace(str, " "=>"") == "Context(Measures.BoundingBox{Tuple{Measures.Length{:w,Float64},Measures.Length{:h,Float64}},Tuple{Measures.Length{:w,Float64},Measures.Length{:h,Float64}}}((0.0w,0.0h),(1.0w,1.0h)),nothing,nothing,nothing,nothing,List([]),List([]),List([]),0,false,false,false,false,nothing,nothing,0.0,Symbol(\"\"))" end @testset "table" begin t = Compose.Table(1, 1, UnitRange(1,1), UnitRange(3:3), aspect_ratio=1.6) io = IOBuffer() # compact printing show(IOContext(io, :compact=>true), t) str = String(take!(io)) @test str == "1x1 Table:\n Context[]\n" # full printing show(io, t) str = String(take!(io)) @test replace(str, " "=>"") == "Compose.Table($(Vector{Context})[[]" * (VERSION>=v"1.7" ? ";;" : "") * "],3:3,1:1,nothing,nothing,1.6,Any[],nothing,0,false,false)" end end # Tagging points(xa, ya) = [(x, y) for (x, y) in zip(xa, ya)] pnts = points(rand(5), rand(5)) p = polygon(pnts, :mypoints) @test p.tag == :mypoints r = rectangle(0, 1, 0.5, 0.8, :box) @test r.tag == :box r = rectangle(rand(5),rand(5),rand(5),rand(5),:manybox) @test r.tag == :manybox c = circle(0, 0.8, 1.2, :circle) @test c.tag == :circle c = circle(rand(5), rand(5), rand(5), :data) @test c.tag == :data elps = ellipse(0, 0.8, 1.2, 1.5, :ellipse) @test elps.tag == :ellipse elps = ellipse(rand(5),rand(5),rand(5),rand(5),:manyellipse) @test elps.tag == :manyellipse txt = text(1.5, 15, "hello", tag=:hello) @test txt.tag == :hello txt = text(rand(5),rand(5),map(x->randstring(5), 1:5), tag=:random) @test txt.tag == :random ln = line(pnts, :line) @test ln.tag == :line crv = curve((0,0), (1,0.5), (0.2,0.3), (0.7,-2.4), :curve) @test crv.tag == :curve crv = curve(pnts, pnts, pnts, pnts, :manycurve) @test crv.tag == :manycurve bm = bitmap("fake", rand(UInt8,10), 0, 1, 0.8, 0.7, :image) @test bm.tag == :image # type definitions & constructors (issue #149) @test isa(Compose.polygon(), Compose.Polygon) @test isa(Compose.polygon([(1,2),(3,5),(4,2)]), Compose.Polygon) @test isa(Compose.rectangle(), Compose.Rectangle) @test isa(Compose.rectangle(0,1,0.3,0.8), Compose.Rectangle) @test isa(Compose.rectangle(rand(5),rand(5),rand(5),rand(5)), Compose.Rectangle) @test isa(Compose.circle(), Compose.Circle) @test isa(Compose.circle(3.2,1.4,0.8), Compose.Circle) @test isa(Compose.circle(rand(5), rand(5), rand(5)), Compose.Circle) @test isa(Compose.ellipse(), Compose.Ellipse) @test isa(Compose.ellipse(0.5,0.5,0.3,0.2), Compose.Ellipse) @test isa(Compose.ellipse(rand(5), rand(5), rand(5), rand(5)), Compose.Ellipse) @test isa(Compose.text(0.5,0.4,"hello"), Compose.Text) @test isa(Compose.text(rand(5),rand(5),["hello","there"]), Compose.Text) @test isa(Compose.line(), Compose.Line) @test isa(Compose.line([(1,2),(3,5),(4,2)]), Compose.Line) # issue #172: default circle(xs, ys, rs) radius measure is context units @test isequal(circle([0.5], [0.5], [0.1]).primitives[1].radius, 0.1cx) # Gadfly issue 857 and 436 # make sure that newlines are respected by `text_extents` font_family = "'PT Sans Caption','Helvetica Neue','Helvetica',sans-serif" oneline = text_extents(font_family, 8pt, "PegPeg")[1] twolines = text_extents(font_family, 8pt, "Peg\nPeg")[1] @test round(Int, oneline[1] / twolines[1]) == 2 @test round(Int, twolines[2] / oneline[2]) == 2 # PR 252 @test Compose.parse_colorant("red") == RGB(1.0,0.0,0.0) @test Compose.parse_colorant(colorant"red") == RGB(1.0,0.0,0.0) @test Compose.parse_colorant(["red","blue"]) == [RGB(1.0,0.0,0.0), RGB(0.0,0.0,1.0)] @test Compose.parse_colorant(("red","blue")) == [RGB(1.0,0.0,0.0), RGB(0.0,0.0,1.0)] @test Compose.parse_colorant("red","blue") == [RGB(1.0,0.0,0.0), RGB(0.0,0.0,1.0)] @test Compose.parse_colorant("red",colorant"blue") == [RGB(1.0,0.0,0.0), RGB(0.0,0.0,1.0)] # PR 263 @test ~ @isdefined Fontconfig @test Compose.pango_to_svg("hello world") == "hello world" import Fontconfig @test Compose.pango_to_svg("hello world") == "hello world" @testset "pango" begin @test Compose.escape_tex_chars("\\") == "\\textbackslash{}" @test Compose.pango_to_pgf("hello\\") == "\\text{hello\\textbackslash{}}" end @testset "pango utf-8 text_extents" begin @test @isdefined Fontconfig w1 = Compose.text_extents("", 8pt, "α")[1] w2 = Compose.text_extents("", 8pt, "αα")[1] @test w1 < w2 end @testset "table" begin @testset "force aspect ratio" begin tbl = Compose.Table(1, 1, UnitRange(1,1), UnitRange(3:3), aspect_ratio=1.6); x_solution = [0.0, 5.8, 11.8] w_solution = [5.8, 6.0, 119.6] y_solution = [0.0, 85.3] h_solution = [85.3, 4.7] x0, w0 = copy(x_solution), copy(w_solution) Compose.force_aspect_ratio!(tbl, x_solution, y_solution, w_solution, h_solution) @test x_solution == x0 @test w_solution == w0 @test y_solution ≈ [5.275, 80.025] @test h_solution == [74.75, 4.7] end end @testset "Image keyword args" begin @test isa(PNG(4inch, 3inch, dpi=172), Compose.Image) end @testset "No Global RNG contamination" begin Random.seed!(23) withoutcompose = rand() Random.seed!(23) draw(SVG(10cm, 8cm, false), compose(context())) withcompose = rand() @test withoutcompose == withcompose end @testset "Image fillopacity" begin properties = [fill(["red","blue"]), fillopacity(0.3), stroke("black")] img1 = PNG(); Compose.push_property_frame(img1, properties) img2 = SVG(); Compose.push_property_frame(img2, properties) img3 = PGF(); Compose.push_property_frame(img3, properties) @test getfield.(img1.vector_properties[Compose.Property{Compose.FillOpacityPrimitive}].primitives, :value) == [0.3, 0.3] @test occursin("fill-opacity=\"0.3\"", String(img2.out.data)) @test img3.fill_opacity == 0.3 end @testset "Missing" begin @test Compose.x_measure(missing)===NaN*cx @test Compose.y_measure(missing)===NaN*cy @test Compose.size_x_measure(missing)===NaN*sx @test Compose.size_y_measure(missing)===NaN*sy end @testset "cx, sx and cy, sy units" begin ub, bb = UnitBox(-0.3, 0., 1.2, 1.), Compose.BoundingBox((50mm, 50mm), (40mm, 40mm)) tr = Compose.IdentityTransform() xpos = (Compose.resolve_position(bb, ub, tr, 0sx), Compose.resolve_position(bb, ub, tr, 0cx)) ub, bb = UnitBox(0., -0.3, 1., 1.2), Compose.BoundingBox((10mm, 10mm), (40mm, 40mm)) ypos = (Compose.resolve_position(bb, ub, tr, 0sy), Compose.resolve_position(bb, ub, tr, 0cy)) @test ypos == xpos == (0.0mm, 10.0mm) end ================================================ FILE: test/runtests.jl ================================================ using Test include("misc.jl") include("svg.jl") include("immerse.jl") # Run the examples cd(joinpath(@__DIR__, "output")) exampledir = joinpath(@__DIR__, "examples") for ex in readdir(exampledir) endswith(ex, ".jl") || continue file = joinpath(exampledir, ex) run(`$(Base.julia_cmd()) --startup-file=no $file`) end ================================================ FILE: test/svg.jl ================================================ using Compose using Test using EzXML using Colors using Measures @testset "Issue 267" begin global c = compose(context(), fill(["red", "blue"]), [context(), fill("green"), circle([0.25, 0.75], [0.5], [0.25])]) img = SVG(8cm, 6cm, false) draw(img, c) svgxml = root(parsexml(String(take!(img.out)))) # get all grouped values that have the fill attribute fillcolors = nodecontent.(findall("//ns:g[@fill]/@fill", svgxml, ["ns"=>namespace(svgxml)])) # there should only be a single color (because green should clobber the red # and blue colors) @test length(fillcolors) == 1 # make sure it's green @test fillcolors[1] == "#008000" end