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 -

- Command window output example from ORinfo -

+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