diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
index 1e036df..2c17caa 100644
--- a/.github/workflows/deploy-docs.yml
+++ b/.github/workflows/deploy-docs.yml
@@ -4,6 +4,14 @@ on:
push:
branches:
- main
+ # Test changes to the image generation or to the plotting code (images
+ # and a comparison with the published site are uploaded as an artifact,
+ # nothing is deployed)
+ pull_request:
+ paths:
+ - docs/scripts/**
+ - src/**
+ - .github/workflows/deploy-docs.yml
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
@@ -12,10 +20,8 @@ permissions:
pages: write
id-token: write
-# Allow only one concurrent deployment
-concurrency:
- group: "pages"
- cancel-in-progress: false
+env:
+ MTEX_VERSION: 7.1.0
jobs:
build:
@@ -23,6 +29,39 @@ jobs:
steps:
- uses: actions/checkout@v4
+ # Plot images for the docs are generated by docs/scripts/make_doc_images.m
+ - uses: matlab-actions/setup-matlab@v2
+ with:
+ release: R2024b
+ products: >
+ Signal_Processing_Toolbox
+ Image_Processing_Toolbox
+ Statistics_and_Machine_Learning_Toolbox
+ cache: true
+
+ - name: Download MTEX
+ run: |
+ curl -sSfL -o "$RUNNER_TEMP/mtex.zip" \
+ "https://github.com/mtex-toolbox/mtex/releases/download/mtex-${MTEX_VERSION}/mtex-${MTEX_VERSION}.zip"
+ unzip -q "$RUNNER_TEMP/mtex.zip" -d "$RUNNER_TEMP/mtex"
+ MTEX_DIR=$(dirname "$(find "$RUNNER_TEMP/mtex" -name startup_mtex.m | head -1)")
+ # mtexdata downloads the example datasets into this folder
+ mkdir -p "$MTEX_DIR/data/EBSD"
+ echo "MTEX_DIR=$MTEX_DIR" >> "$GITHUB_ENV"
+
+ # Some ORTools functions dock their figures, which MATLAB only allows
+ # with a display, so provide a virtual one
+ - name: Start virtual display
+ run: |
+ sudo apt-get install -y -qq xvfb > /dev/null
+ Xvfb :99 -screen 0 1920x1200x24 &
+ echo "DISPLAY=:99" >> "$GITHUB_ENV"
+
+ - name: Generate documentation images
+ uses: matlab-actions/run-command@v2
+ with:
+ command: addpath(getenv('MTEX_DIR')); startup_mtex; addpath('docs/scripts'); make_doc_images
+
- uses: actions/setup-python@v5
with:
python-version: 3.x
@@ -41,14 +80,36 @@ jobs:
run: pip install -r docs/requirements.txt
- name: Build site
- run: mkdocs build
+ run: mkdocs build --strict
+
+ - name: Compare with the published images
+ if: github.event_name == 'pull_request'
+ run: |
+ pip install pillow
+ python docs/scripts/compare_with_site.py
+
+ - name: Upload generated images for review
+ if: github.event_name == 'pull_request'
+ uses: actions/upload-artifact@v4
+ with:
+ name: doc-images
+ path: |
+ docs/images/
+ docs/snippets/
+ docs-review/
- name: Upload artifact
+ if: github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@v3
with:
path: site/
deploy:
+ if: github.event_name != 'pull_request'
+ # Allow only one concurrent deployment
+ concurrency:
+ group: "pages"
+ cancel-in-progress: false
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
diff --git a/.gitignore b/.gitignore
index e5be810..1519ab5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,3 +51,19 @@ desktop.ini
# MkDocs build output
site/
+
+# Documentation images and snippets generated by docs/scripts/make_doc_images.m.
+# Only screenshots of GUIs and the logos are kept in the repository.
+docs/images/*
+!docs/images/ORTools.png
+!docs/images/ORTools_white.png
+!docs/images/computeGrains.png
+!docs/images/grainClick.png
+!docs/images/guiOR.png
+!docs/images/peakFitORs.png
+!docs/images/recolorPhases.png
+docs/snippets/
+docs-review/
+
+# Python
+__pycache__/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0298640..9530ae4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -48,6 +48,20 @@ mkdocs serve
CI builds the site with `mkdocs build --strict`, so broken links fail the pull request.
+The plot images in `docs/images` and the command window output in `docs/snippets` are not stored in
+the repository. They are generated by `docs/scripts/make_doc_images.m` whenever the site is
+deployed. To generate them locally (MATLAB with MTEX, plus the Signal Processing, Image Processing and
+Statistics toolboxes):
+
+```matlab
+startup_mtex
+addpath('docs/scripts')
+make_doc_images
+```
+
+When documenting a new plotting function, add a call for it to `make_doc_images.m`. Screenshots of
+GUIs and interactive windows cannot be generated; they are the only images kept in `docs/images`.
+
## Code of conduct
This project follows the [Code of Conduct](CODE_OF_CONDUCT.md).
diff --git a/docs/function_index.md b/docs/function_index.md
index d03e5d4..3e75fdb 100644
--- a/docs/function_index.md
+++ b/docs/function_index.md
@@ -259,9 +259,11 @@ The function extracts orientation relationship (OR) information contained in the
- Options
- silent - suppress command window output
-
-
-
+Example command window output:
+
+```text
+--8<-- "ORinfo.txt"
+```
## P
diff --git a/docs/images/ORinfo.png b/docs/images/ORinfo.png
deleted file mode 100644
index cfa9bcf..0000000
Binary files a/docs/images/ORinfo.png and /dev/null differ
diff --git a/docs/images/computeParentTwins.png b/docs/images/computeParentTwins.png
deleted file mode 100644
index faad49c..0000000
Binary files a/docs/images/computeParentTwins.png and /dev/null differ
diff --git a/docs/images/computehabitPlane.png b/docs/images/computehabitPlane.png
deleted file mode 100644
index 99d7ae6..0000000
Binary files a/docs/images/computehabitPlane.png and /dev/null differ
diff --git a/docs/images/plotHist_ORMisfit.png b/docs/images/plotHist_ORMisfit.png
deleted file mode 100644
index 9c639bd..0000000
Binary files a/docs/images/plotHist_ORMisfit.png and /dev/null differ
diff --git a/docs/images/plotHist_OR_misfit.png b/docs/images/plotHist_OR_misfit.png
deleted file mode 100644
index d5defe9..0000000
Binary files a/docs/images/plotHist_OR_misfit.png and /dev/null differ
diff --git a/docs/images/plotIPDF_gB_misfit.png b/docs/images/plotIPDF_gB_misfit.png
deleted file mode 100644
index 5a06e4d..0000000
Binary files a/docs/images/plotIPDF_gB_misfit.png and /dev/null differ
diff --git a/docs/images/plotIPDF_gB_prob.png b/docs/images/plotIPDF_gB_prob.png
deleted file mode 100644
index 72e31a5..0000000
Binary files a/docs/images/plotIPDF_gB_prob.png and /dev/null differ
diff --git a/docs/images/plotMap_IPF_p2c.png b/docs/images/plotMap_IPF_p2c.png
deleted file mode 100644
index 7f33286..0000000
Binary files a/docs/images/plotMap_IPF_p2c.png and /dev/null differ
diff --git a/docs/images/plotMap_KSvariantPairs.png b/docs/images/plotMap_KSvariantPairs.png
deleted file mode 100644
index 2a41ea7..0000000
Binary files a/docs/images/plotMap_KSvariantPairs.png and /dev/null differ
diff --git a/docs/images/plotMap_bain.png b/docs/images/plotMap_bain.png
deleted file mode 100644
index 1ee0e00..0000000
Binary files a/docs/images/plotMap_bain.png and /dev/null differ
diff --git a/docs/images/plotMap_blockWidths.png b/docs/images/plotMap_blockWidths.png
deleted file mode 100644
index 227d1c7..0000000
Binary files a/docs/images/plotMap_blockWidths.png and /dev/null differ
diff --git a/docs/images/plotMap_clusters.png b/docs/images/plotMap_clusters.png
deleted file mode 100644
index df1a8d4..0000000
Binary files a/docs/images/plotMap_clusters.png and /dev/null differ
diff --git a/docs/images/plotMap_gB_c2c.png b/docs/images/plotMap_gB_c2c.png
deleted file mode 100644
index 09c7bf3..0000000
Binary files a/docs/images/plotMap_gB_c2c.png and /dev/null differ
diff --git a/docs/images/plotMap_gB_misfit.png b/docs/images/plotMap_gB_misfit.png
deleted file mode 100644
index 2517cde..0000000
Binary files a/docs/images/plotMap_gB_misfit.png and /dev/null differ
diff --git a/docs/images/plotMap_gB_p2c.png b/docs/images/plotMap_gB_p2c.png
deleted file mode 100644
index 39f32dc..0000000
Binary files a/docs/images/plotMap_gB_p2c.png and /dev/null differ
diff --git a/docs/images/plotMap_gB_prob.png b/docs/images/plotMap_gB_prob.png
deleted file mode 100644
index 230dbcd..0000000
Binary files a/docs/images/plotMap_gB_prob.png and /dev/null differ
diff --git a/docs/images/plotMap_packets.png b/docs/images/plotMap_packets.png
deleted file mode 100644
index 8313225..0000000
Binary files a/docs/images/plotMap_packets.png and /dev/null differ
diff --git a/docs/images/plotMap_phases.png b/docs/images/plotMap_phases.png
deleted file mode 100644
index 174a40e..0000000
Binary files a/docs/images/plotMap_phases.png and /dev/null differ
diff --git a/docs/images/plotMap_variants.png b/docs/images/plotMap_variants.png
deleted file mode 100644
index 916813a..0000000
Binary files a/docs/images/plotMap_variants.png and /dev/null differ
diff --git a/docs/images/plotPDF_packets.png b/docs/images/plotPDF_packets.png
deleted file mode 100644
index 2f03db1..0000000
Binary files a/docs/images/plotPDF_packets.png and /dev/null differ
diff --git a/docs/images/plotPDF_variants.png b/docs/images/plotPDF_variants.png
deleted file mode 100644
index 90fa83c..0000000
Binary files a/docs/images/plotPDF_variants.png and /dev/null differ
diff --git a/docs/images/plotPODF_transformation.png b/docs/images/plotPODF_transformation.png
deleted file mode 100644
index d578972..0000000
Binary files a/docs/images/plotPODF_transformation.png and /dev/null differ
diff --git a/docs/images/plotStack.png b/docs/images/plotStack.png
deleted file mode 100644
index ba87296..0000000
Binary files a/docs/images/plotStack.png and /dev/null differ
diff --git a/docs/scripts/compare_with_site.py b/docs/scripts/compare_with_site.py
new file mode 100644
index 0000000..6e23b90
--- /dev/null
+++ b/docs/scripts/compare_with_site.py
@@ -0,0 +1,98 @@
+"""Compare generated documentation images with the published ones.
+
+For every PNG in docs/images, download the image of the same name from the
+published documentation site and write a side-by-side comparison (published
+left, generated right) to docs-review/compare/, plus an index.html listing
+them all. Images identical to the published ones are skipped. Used by the
+deploy workflow on pull requests.
+"""
+
+import html
+import io
+import sys
+import urllib.error
+import urllib.request
+from pathlib import Path
+
+from PIL import Image, ImageDraw, ImageFont
+
+SITE = "https://ortools4mtex.github.io/ORTools/images/"
+ROOT = Path(__file__).resolve().parents[2]
+IMAGES = ROOT / "docs" / "images"
+OUT = ROOT / "docs-review" / "compare"
+HEIGHT = 700 # both images are scaled to this height
+LABEL = 40
+
+
+def published(name):
+ """Return the bytes of the published image, or None if there is none."""
+ try:
+ with urllib.request.urlopen(SITE + name, timeout=30) as response:
+ return response.read()
+ except urllib.error.HTTPError as err:
+ if err.code == 404:
+ return None
+ raise
+
+
+def scaled(img):
+ width = round(img.width * HEIGHT / img.height)
+ return img.resize((width, HEIGHT), Image.LANCZOS)
+
+
+def placeholder(text):
+ img = Image.new("RGB", (HEIGHT, HEIGHT), "white")
+ ImageDraw.Draw(img).text((20, HEIGHT // 2), text, fill="gray", font=font(28))
+ return img
+
+
+def font(size):
+ try:
+ return ImageFont.truetype("DejaVuSans.ttf", size)
+ except OSError:
+ return ImageFont.load_default()
+
+
+def side_by_side(left, right):
+ panels = [("Published", left), ("Generated", right)]
+ width = sum(img.width for _, img in panels) + 20
+ out = Image.new("RGB", (width, HEIGHT + LABEL), "white")
+ draw = ImageDraw.Draw(out)
+ x = 0
+ for label, img in panels:
+ draw.text((x + 10, 5), label, fill="black", font=font(28))
+ out.paste(img, (x, LABEL))
+ x += img.width + 20
+ return out
+
+
+def main():
+ OUT.mkdir(parents=True, exist_ok=True)
+ rows = []
+ for path in sorted(IMAGES.glob("*.png")):
+ data = published(path.name)
+ if data == path.read_bytes():
+ continue # unchanged, e.g. the logos and GUI screenshots
+ old = None if data is None else Image.open(io.BytesIO(data)).convert("RGB")
+ new = Image.open(path).convert("RGB")
+ status = "new image" if old is None else f"{old.width}x{old.height} -> {new.width}x{new.height}"
+ left = placeholder("not on the site") if old is None else scaled(old)
+ side_by_side(left, scaled(new)).save(OUT / path.name)
+ rows.append((path.name, status))
+ print(f"{path.name}: {status}")
+
+ items = "\n".join(
+ f'{html.escape(name)} ({html.escape(status)})
\n'
+ f'
'
+ for name, status in rows
+ )
+ (OUT / "index.html").write_text(
+ "Documentation images\n"
+ "\n"
+ "Published (left) vs generated (right)
\n" + items + "\n",
+ encoding="utf-8",
+ )
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/docs/scripts/make_doc_images.m b/docs/scripts/make_doc_images.m
new file mode 100644
index 0000000..b2b3add
--- /dev/null
+++ b/docs/scripts/make_doc_images.m
@@ -0,0 +1,334 @@
+function make_doc_images(outDir)
+ %% Function description:
+ % This function generates the plot images and text snippets used by the
+ % documentation. It runs the ORTools functions on MTEX example datasets
+ % (and on the TRWIP steel dataset in data/input/ebsd) without any user
+ % interaction and saves one PNG per function to docs/images.
+ %
+ % The screenshots of GUIs (computeGrains, guiOR, grainClick,
+ % recolorPhases and the interactive peak fitting window shown for
+ % peakFitORs) cannot be generated and are kept in the repository.
+ %
+ %% Syntax:
+ % make_doc_images
+ % make_doc_images(outDir)
+ %
+ %% Input:
+ % outDir - folder the images are written to (default: docs/images)
+ %
+ %% Output files:
+ % /.png - image used by the documentation
+ % docs-review/_.png - every figure a function opened, for
+ % choosing which ones form the image
+ %
+ %% Requirements:
+ % MTEX on the MATLAB path (run startup_mtex first), plus the Signal
+ % Processing, Image Processing and Statistics toolboxes
+
+ root = fileparts(fileparts(fileparts(mfilename('fullpath'))));
+ if nargin < 1
+ outDir = fullfile(root, 'docs', 'images');
+ end
+ snippetDir = fullfile(root, 'docs', 'snippets');
+
+ % Put ORTools on the path
+ addpath(root);
+ currentFolder;
+
+ dirs.images = outDir;
+ dirs.review = fullfile(root, 'docs-review');
+ ensureFolder(fullfile(dirs.images, 'x'));
+ ensureFolder(fullfile(dirs.review, 'x'));
+ ensureFolder(fullfile(snippetDir, 'x'));
+
+ set(groot, 'DefaultFigureVisible', 'off');
+ set(groot, 'DefaultFigureWindowStyle', 'normal');
+ setMTEXpref('xAxisDirection', 'east');
+ setMTEXpref('zAxisDirection', 'outOfPlane');
+ setMTEXpref('FontSize', 14);
+ setInterp2Tex;
+
+ failed = {};
+
+ %% Lath martensite (MTEX dataset 'martensite')
+ screenPrint('SegmentStart', 'Lath martensite');
+ ebsd = mtexdata('martensite');
+ [grains, ebsd] = calcGrains(ebsd('indexed'), 'angle', 3 * degree);
+ ebsd(grains(grains.numPixel < 3)) = [];
+ [grains, ebsd] = calcGrains(ebsd('indexed'), 'angle', 3 * degree);
+ grains = smoothBoundary(grains, 5);
+ ebsd = renameByMineral(ebsd, {'fcc', 'Gamma'; 'bcc', 'AlphaP'});
+ job = defineJob(ebsd, grains, 'Gamma', 'AlphaP');
+
+ KS = orientation.KurdjumovSachs(job.csParent, job.csChild);
+ NW = orientation.NishiyamaWassermann(job.csParent, job.csChild);
+ job.p2c = KS;
+ job.calcParent2Child('local');
+
+ failed = snap(failed, dirs, 'plotHist_OR_misfit', ...
+ @() plotHist_OR_misfit(job, [KS, NW], 'legend', {'K-S OR', 'N-W OR'}));
+ failed = saveText(failed, snippetDir, 'ORinfo', @() ORinfo(job.p2c));
+ failed = snap(failed, dirs, 'plotMap_gB_misfit', ...
+ @() plotMap_gB_misfit(job, 'linewidth', 2, 'maxColor', 5));
+ failed = snap(failed, dirs, 'plotMap_gB_prob', ...
+ @() plotMap_gB_prob(job, 'threshold', 2.5 * degree, 'tolerance', 2.5 * degree, 'linewidth', 2));
+
+ % Clusters of the grain graph (as in Example 1), on a separate job
+ jobGraph = defineJob(ebsd, grains, 'Gamma', 'AlphaP');
+ jobGraph.p2c = job.p2c;
+ jobGraph.calcGraph('threshold', 2.5 * degree, 'tolerance', 2.5 * degree);
+ jobGraph.clusterGraph('inflationPower', 1.6);
+ failed = snap(failed, dirs, 'plotMap_clusters', ...
+ @() plotMap_clusters(jobGraph, 'linewidth', 2));
+
+ % Reconstruction with the variant graph approach (as in Example 7)
+ job.calcVariantGraph('threshold', 4 * degree, 'tolerance', 3.5 * degree);
+ job.clusterVariantGraph;
+ job.calcParentFromVote('minProb', 0.5);
+ job.calcGBVotes('p2c', 'reconsiderAll');
+ job.calcParentFromVote;
+ job.mergeSimilar('threshold', 7.5 * degree);
+ job.mergeInclusions('maxSize', 150);
+ job.calcVariants;
+
+ % Id (not index) of the largest reconstructed parent grain
+ [~, maxParentIdx] = max(job.parentGrains.area);
+ maxParentId = job.parentGrains.id(maxParentIdx);
+
+ failed = snap(failed, dirs, 'plotPDF_variants', @() plotPDF_variants(job));
+ failed = snap(failed, dirs, 'plotPDF_packets', @() plotPDF_packets(job));
+ failed = snap(failed, dirs, 'plotPDF_bain', @() plotPDF_bain(job));
+ failed = snap(failed, dirs, 'plotMap_variants', ...
+ @() plotMap_variants(job, 'linewidth', 1));
+ failed = snap(failed, dirs, 'plotMap_packets', ...
+ @() plotMap_packets(job, 'linewidth', 3));
+ failed = snap(failed, dirs, 'plotMap_bain', ...
+ @() plotMap_bain(job, 'linewidth', 2, 'colormap', magma));
+ failed = snap(failed, dirs, 'plotMap_KSvariantPairs', ...
+ @() plotMap_KSvariantPairs(job, 'parentGrainId', maxParentId, 'linewidth', 2));
+ failed = snap(failed, dirs, 'plotMap_blockWidths', ...
+ @() plotMap_blockWidths(job, 'parentGrainId', maxParentId, 'linewidth', 1.5));
+ failed = snap(failed, dirs, 'plotStack', ...
+ @() plotStack(job, 'parentGrainId', maxParentId, 'linewidth', 1.5), 'grid5');
+ failed = snap(failed, dirs, 'computehabitPlane', ...
+ @() computeHabitPlane(job, 'Shape', 'minClusterSize', 50, 'reliability', 0.5, 'plotTraces'));
+ failed = snap(failed, dirs, 'computeParentTwins', ...
+ @() computeParentTwins(job, maxParentId));
+
+ % Texture transformation of the reconstructed parent texture
+ hParent = [Miller(1, 1, 1, job.csParent), Miller(2, 0, 0, job.csParent), Miller(2, 2, 0, job.csParent)];
+ hChild = [Miller(1, 1, 0, job.csChild), Miller(2, 0, 0, job.csChild), Miller(2, 1, 1, job.csChild)];
+ inputODF = calcDensity(job.parentEBSD.orientations); %#ok
+ pfNameIn = fullfile(tempdir, 'ORTools_inputTexture.mat');
+ pfNameOut = fullfile(tempdir, 'ORTools_outputTexture.mat');
+ save(pfNameIn, 'inputODF');
+ % Figures: 1 parent PFs, 2 parent ODF, 3 child PFs, 4 child ODF
+ failed = snap(failed, dirs, 'plotPODF_transformation', ...
+ @() plotPODF_transform(job, hParent, hChild, 'import', pfNameIn, 'export', pfNameOut), [1 3; 2 4]);
+
+ %% Alpha-beta titanium (MTEX dataset 'alphaBetaTitanium')
+ screenPrint('SegmentStart', 'Alpha-beta titanium');
+ ebsd = mtexdata('alphaBetaTitanium');
+ [grains, ebsd] = calcGrains(ebsd('indexed'), 'threshold', 1.5 * degree, ...
+ 'removeQuadruplePoints');
+ ebsd = renameByMineral(ebsd, {'alpha', 'Alpha'; 'beta', 'Beta'});
+ job = defineJob(ebsd, grains, 'Beta', 'Alpha');
+ job.p2c = orientation.Burgers(job.csParent, job.csChild);
+
+ failed = snap(failed, dirs, 'plotIPDF_gB_misfit', @() plotIPDF_gB_misfit(job, 'colormapP', jet), 1:3);
+ failed = snap(failed, dirs, 'plotIPDF_gB_prob', @() plotIPDF_gB_prob(job), 1:3);
+
+ %% TRWIP steel (data/input/ebsd/TRWIPsteel.ctf)
+ screenPrint('SegmentStart', 'TRWIP steel');
+ ebsd = loadEBSD_ctf(fullfile(root, 'data', 'input', 'ebsd', 'TRWIPsteel.ctf'), ...
+ 'convertSpatial2EulerReferenceFrame');
+ ebsd = ebsd('indexed');
+ [grains, ebsd] = calcGrains(ebsd('indexed'), 'threshold', 3 * degree, ...
+ 'removeQuadruplePoints');
+ ebsd = renameByMineral(ebsd, {'fcc', 'Gamma'; 'bcc', 'AlphaP'; 'epsilon', 'Epsilon'});
+ job = defineJob(ebsd, grains, 'Gamma', 'AlphaP');
+
+ failed = snap(failed, dirs, 'plotMap_phases', @() plotMap_phases(job, 'linewidth', 2));
+ % Epsilon martensite as parent of alpha' martensite
+ jobEpsilon = defineJob(ebsd, grains, 'Epsilon', 'AlphaP');
+ failed = snap(failed, dirs, 'plotMap_IPF_p2c', ...
+ @() plotMap_IPF_p2c(jobEpsilon, vector3d.Z, 'linewidth', 2));
+ failed = snap(failed, dirs, 'plotMap_gB_c2c', ...
+ @() plotMap_gB_c2c(job, 'linewidth', 2));
+ failed = snap(failed, dirs, 'plotMap_gB_p2c', @() plotMap_gB_p2c(job, 'linewidth', 1.5));
+
+ %% Report
+ if isempty(failed)
+ screenPrint('SegmentStart', 'All documentation images were generated');
+ else
+ error('make_doc_images:failed', 'Failed to generate:\n %s', strjoin(failed, '\n '));
+ end
+end
+
+function failed = snap(failed, dirs, name, plotFun, panels)
+ %% Run a plotting function and save the figure(s) it opens as PNG
+ % Every figure is saved to the review folder as _.png. The
+ % documentation image .png is made of the figures in "panels":
+ % n - figure number(s), placed side by side (default: 1)
+ % [1 3; 2 4] - a grid, one matrix row per image row (0 = empty)
+ % 'gridN' - all figures, N per row, each fitted into a cell of the
+ % same size
+ if nargin < 5
+ panels = 1;
+ end
+ close all;
+ try
+ plotFun();
+ drawnow;
+ figs = findall(groot, 'Type', 'figure');
+ if isempty(figs)
+ error('no figure was created');
+ end
+ [~, order] = sort(arrayfun(@figureOrder, figs));
+ figs = figs(order);
+ for ii = 1:numel(figs)
+ % Docked figures that were never shown are not drawn, so bring
+ % each one to the front before exporting it
+ figure(figs(ii));
+ drawnow;
+ exportgraphics(figs(ii), reviewFile(dirs, name, ii), 'Resolution', 150);
+ end
+ cellSize = [];
+ if ischar(panels)
+ nCols = str2double(panels(5:end));
+ cellSize = [450 600];
+ panels = 1:numel(figs);
+ panels(end + 1:nCols * ceil(numel(figs) / nCols)) = 0;
+ panels = reshape(panels, nCols, []).';
+ end
+ panels(panels > numel(figs)) = 0;
+ rows = cell(size(panels, 1), 1);
+ for rr = 1:size(panels, 1)
+ imgs = arrayfun(@(ii) readPanel(dirs, name, ii), panels(rr, :), 'UniformOutput', false);
+ if ~isempty(cellSize)
+ imgs = cellfun(@(im) fitCell(im, cellSize), imgs, 'UniformOutput', false);
+ end
+ rows{rr} = sideBySide(imgs);
+ end
+ imwrite(stacked(rows), fullfile(dirs.images, [name, '.png']));
+ screenPrint('Step', sprintf('Saved %s (figure(s) %s of %d)', name, mat2str(panels), numel(figs)));
+ catch err
+ failed{end + 1} = sprintf('%s: %s', name, err.message);
+ screenPrint('Step', sprintf('FAILED %s: %s', name, err.message));
+ end
+ close all;
+end
+
+function fileName = reviewFile(dirs, name, ii)
+ %% File name of the ii-th figure of a function in the review folder
+ fileName = fullfile(dirs.review, sprintf('%s_%d.png', name, ii));
+end
+
+function img = readPanel(dirs, name, ii)
+ %% Image of the ii-th figure of a function (empty for 0)
+ img = uint8([]);
+ if ii > 0
+ img = imread(reviewFile(dirs, name, ii));
+ end
+end
+
+function img = fitCell(im, cellSize)
+ %% Scale an image to fit a cell of [height width] and centre it on white
+ % (an empty image gives an empty white cell, which keeps the grid aligned)
+ if isempty(im)
+ img = 255 * ones([cellSize, 3], 'uint8');
+ return
+ end
+ scale = min(cellSize ./ [size(im, 1), size(im, 2)]);
+ im = imresize(im, min(cellSize, round(scale * [size(im, 1), size(im, 2)])));
+ img = 255 * ones([cellSize, 3], 'like', im);
+ top = floor((cellSize(1) - size(im, 1)) / 2);
+ left = floor((cellSize(2) - size(im, 2)) / 2);
+ img(top + (1:size(im, 1)), left + (1:size(im, 2)), :) = im;
+end
+
+function img = stacked(rows)
+ %% Place images below each other, centred horizontally on white
+ width = max(cellfun(@(im) size(im, 2), rows));
+ for ii = 1:numel(rows)
+ im = rows{ii};
+ padding = width - size(im, 2);
+ left = floor(padding / 2);
+ white = @(cols) 255 * ones(size(im, 1), cols, 3, 'like', im);
+ rows{ii} = [white(left), im, white(padding - left)];
+ end
+ img = vertcat(rows{:});
+end
+
+function img = sideBySide(imgs)
+ %% Place RGB images next to each other, centred vertically on white
+ imgs = imgs(~cellfun(@isempty, imgs));
+ height = max(cellfun(@(im) size(im, 1), imgs));
+ for ii = 1:numel(imgs)
+ im = imgs{ii};
+ if size(im, 3) == 1
+ im = repmat(im, 1, 1, 3);
+ end
+ padding = height - size(im, 1);
+ top = floor(padding / 2);
+ white = @(rows) 255 * ones(rows, size(im, 2), 3, 'like', im);
+ imgs{ii} = [white(top); im; white(padding - top)];
+ end
+ img = [imgs{:}];
+end
+
+function n = figureOrder(fig)
+ %% Figure number for sorting (figures without a number go last)
+ n = fig.Number;
+ if isempty(n)
+ n = Inf;
+ end
+end
+
+function failed = saveText(failed, snippetDir, name, printFun)
+ %% Capture the command window output of a function as a text snippet
+ try
+ txt = evalc('printFun()');
+ fid = fopen(fullfile(snippetDir, [name, '.txt']), 'w', 'n', 'UTF-8');
+ fprintf(fid, '%s', strtrim(txt));
+ fclose(fid);
+ screenPrint('Step', sprintf('Saved %s.txt', name));
+ catch err
+ failed{end + 1} = sprintf('%s: %s', name, err.message);
+ screenPrint('Step', sprintf('FAILED %s: %s', name, err.message));
+ end
+end
+
+function ebsd = renameByMineral(ebsd, renames)
+ %% Rename phases whose mineral name contains a given pattern
+ % renames - {pattern, newName; ...}, matched case-insensitively
+ % (the same renaming as renamePhases, without the selection dialog)
+ for ii = 2:numel(ebsd.mineralList)
+ for jj = 1:size(renames, 1)
+ if contains(lower(ebsd.mineralList{ii}), lower(renames{jj, 1}))
+ ebsd.CSList(ii).mineral = renames{jj, 2};
+ break
+ end
+ end
+ end
+end
+
+function job = defineJob(ebsd, grains, parentName, childName)
+ %% Define a parentGrainReconstructor job for named phases
+ % (the same as setParentGrainReconstructor, without the selection dialog)
+ csParent = findPhase(ebsd, parentName);
+ csChild = findPhase(ebsd, childName);
+ p2c0 = orientation.byEuler(0, 0, 0, csParent, csChild);
+ job = parentGrainReconstructor(ebsd, grains, p2c0);
+end
+
+function cs = findPhase(ebsd, name)
+ %% Crystal symmetry of the phase with the given mineral name
+ for ii = 2:numel(ebsd.mineralList)
+ if strcmp(ebsd.mineralList{ii}, name)
+ cs = ebsd.CSList(ii);
+ return
+ end
+ end
+ error('make_doc_images:phase', 'Phase ''%s'' not found', name);
+end
diff --git a/mkdocs.yml b/mkdocs.yml
index 54d5071..ab455e0 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -13,3 +13,11 @@ nav:
- Introduction: index.md
- Examples: examples.md
- Function Index: function_index.md
+markdown_extensions:
+ # Command window output captured by docs/scripts/make_doc_images.m
+ - pymdownx.snippets:
+ base_path: [docs/snippets]
+exclude_docs: |
+ scripts/
+ snippets/
+ requirements.txt