Reverse Plot Template
Overview
Reverse engineer a scientific plot from a bitmap or PDF into reproducible artifacts and template parameters:
- A runnable per-plot Python reconstruction script and rendered PNG.
- Editable point/curve CSV files when data must be digitized from pixels.
- A pixel-calibrated OriginPro
.opjuproject and Origin-exported PNG when OriginPro automation is available. - Origin, Matplotlib, and MATLAB parameter tables/snippets when the user asks for templates rather than files.
Always state that reconstructed data are estimates unless source data are available. For PDFs, distinguish vector measurements from rasterized estimates.
Workflow
- Identify the input type and whether the user asks for parameter advice only or direct reconstruction files. If unspecified, produce all direct reconstruction artifacts plus the three parameter recipes.
- For PDF input, follow PDF Input Workflow to locate the figure, render or extract it, and create
<slug>_source_crop.png. For image input, use the supplied image directly. - Run
scripts/analyze_plot_bitmap.pyon the source image or PDF figure crop:
python <skill_dir>/scripts/analyze_plot_bitmap.py "<image_path>" --markdown work/plot_bitmap_analysis.md --json work/plot_bitmap_analysis.json
- Inspect the analysis and source visually. Correct automation mistakes such as text counted as line color, missing borders, panel confusion, or cropped labels.
- Digitize or estimate the visible data. Preserve separate arrays for fitted curves, observed points, error bars, and each panel when the source distinguishes them.
- For a direct reconstruction request, create and run the per-plot artifact script described below. Do not stop at a code snippet or parameter table.
- For an OriginPro recreation request, use the pixel-calibrated workflow below instead of Origin's default page/template scale.
- Return artifact paths, measured dimensions, uncertainty, and only the requested template details.
PDF Input Workflow
Treat a PDF as a container, not as an image. A vector PDF has no intrinsic pixel size, so pixel-level matching is defined at a fixed render DPI and crop rectangle.
- Inspect document metadata and page count with
pdfinfo. Usepdftotext,pypdf, orpdfplumberonly to locate the figure number and caption; never infer layout from extracted text alone. - Map the requested figure to candidate pages, then render those pages for visual confirmation. Poppler page numbers are 1-based; PyMuPDF page indices are 0-based.
pdfinfo "<input.pdf>"
pdftoppm -f <page> -l <page> -singlefile -r 300 -png "<input.pdf>" "work/<slug>_page"
- If Poppler is unavailable or fails on a non-ASCII Windows path, copy the PDF to a short ASCII temporary path without modifying the source, then retry. Use PyMuPDF as the fallback renderer:
import fitz
doc = fitz.open(input_pdf)
page = doc[page_number_1_based - 1]
clip = fitz.Rect(x0_pt, y0_pt, x1_pt, y1_pt)
pix = page.get_pixmap(matrix=fitz.Matrix(dpi / 72, dpi / 72), clip=clip, alpha=False)
pix.save(source_crop_png)
- Inspect
page.get_drawings(),page.get_text("dict"), andpage.get_images()when available. Use vector objects and embedded text to measure strokes, colors, fonts, and positions; use the rendered crop for final visual calibration. Do not assume the full figure is one embedded image. - Determine the graphical figure bounding box in PDF points. Exclude the caption unless the user asks to reproduce it. Render that exact rectangle to
outputs/<slug>_source_crop.pngat a fixed DPI, and record the page number, DPI, PDF-point crop, and resulting pixel size in<slug>_recreate.py. - For a multi-panel figure, record one pixel rectangle per panel and recreate each panel as a separate Matplotlib axes and Origin graph layer. Include inset axes as independent editable layers.
- Run the bitmap analyzer on the crop, then use the normal measurement, digitization, OriginPro, Matplotlib, and MATLAB workflows below.
- If the PDF is scanned or the figure is raster-only, state that all geometry, text, and data are raster estimates. If vector objects are recoverable, state which parameters came from vectors and which were visually inferred.
PDF verification contract:
- Re-render the same PDF crop at the declared DPI before calibration.
- Match the reconstruction canvas to the crop pixel dimensions exactly.
- Verify every panel rectangle, legend, inset, annotation, and visible curve.
- Keep the reconstruction editable; never use the crop as a hidden background substitute.
Direct Reconstruction Artifacts
Unless the user explicitly asks for parameters only, create these files in the workspace outputs/ directory using a short slug for the figure:
<slug>_recreate.py
<slug>_digitized_points.csv
<slug>_fitted_curves.csv
<slug>_recreated_python.png
<slug>_recreated_originpro.opju
<slug>_recreated_originpro.png
For PDF input, also keep outputs/<slug>_source_crop.png as the fixed-DPI audit reference. A rendered full page is temporary unless it is needed to document ambiguous figure boundaries.
Put all per-figure data arrays, colors, geometry, Matplotlib rendering, and optional OriginPro automation in <slug>_recreate.py. Use PchipInterpolator or another shape-preserving fit for smooth curves when the bitmap shows fitted curves; do not make the curve pass through visibly off-curve observations merely to reduce code.
Implement two paths in that script:
python <slug>_recreate.py # write CSV files and the Python PNG
python <slug>_recreate.py --origin # additionally write the .opju and Origin PNG
Use Matplotlib with an explicit figsize=(width_px/dpi, height_px/dpi), DPI, and subplots_adjust values derived from the plot frame. Use the same digitized arrays for OriginPro. When originpro is importable and OriginPro automation succeeds, create a workbook, add fitted curve plots, pale error-bar plots, dark point plots, configure the calibrated page/layer, export the Origin PNG, and save the .opju.
If OriginPro automation is absent or fails, still emit the Python script, PNG, CSVs, and an Origin parameter table, but do not claim that an .opju was created. Do not embed the source bitmap as a background merely to claim a pixel-level editable reconstruction.
After generation, run both script paths when OriginPro is available. Verify that the PNG dimensions equal the target image or PDF crop, the required artifact files are nonempty, and every Origin plot frame lands at its measured pixel rectangle. Inspect both Python and Origin exports before handoff.
Measurement Rules
- Use the plot frame as the coordinate reference whenever it is detected. Report frame left/right/top/bottom in pixels and percentages of the image.
- Convert label and guide positions to data coordinates only when axis limits are readable or inferable. Otherwise report them as layer percentages.
- Estimate line widths from rendered pixels and convert to points with
pt ~= px * 72 / dpi. If DPI is unknown, use the visible relation:2 pxin a 300-600 px wide bitmap usually corresponds to1.2-1.8 ptin Origin/Matplotlib/MATLAB. - Prefer exact sampled RGB colors from the image for curve colors. Report both RGB and hex.
- Treat font family as an inference. For scientific plots with sans-serif glyphs, start with
Arialfor Origin and MATLAB, andArial/DejaVu Sansfor Matplotlib. - Estimate font sizes by bounding-box height:
- Axis title: usually
14-18 pt. - Tick labels: usually
9-12 pt. - Curve labels and annotations: usually
8-11 pt.
- Axis title: usually
- Distinguish the objective measurement from the recommendation. Example: "measured curve stroke is about 2 px; set Origin line width to 1.5 pt."
Origin Template Output
Use this structure when returning Origin settings:
Page and Layer
- Page size: <width> mm x <height> mm, or keep original aspect ratio <W:H>
- Layer position: Left <percent>%, Top <percent>%, Width <percent>%, Height <percent>%
- Background: White
- Border/frame: show left/right/top/bottom, black, <pt> pt
Axes and Ticks
- X scale: From <min> To <max>, Major increment <step>
- Y scale: <hidden/readable/inferred>
- Tick direction: Out
- Major tick length: <pt> pt
- Tick/axis width: <pt> pt
- Tick label font: Arial, <pt> pt
- X title / Y title: text, font, size, style
Plots
- Plot name: color <hex> RGB(...), line width <pt>, symbol none
- Repeat for each curve
Guide Lines and Annotations
- Guide line: x=<data or layer %>, y start/end, black, width <pt>, dash pattern
- Text: content, font, size, position, alignment
Export
- Pixel size or DPI, anti-aliasing, transparent background if needed
For Origin rich text, use common escape forms when helpful:
- Greek chi:
\g(c) - Superscript:
\+(text) - Subscript:
\-(text) - Italic variable:
\i(text)
Pixel-Calibrated OriginPro Workflow
Use this workflow when the user asks for a faithful editable OriginPro reconstruction, especially after an initial Origin graph has visibly wrong font sizes, stroke widths, or page proportions.
- Isolate the chart page from any palette or caption below it. Record the target
width_px,height_px, and plot frame pixel rectangle. - Choose a fixed export DPI. Use the source DPI when known; otherwise start at
180. Computewidth_in = width_px / dpiandheight_in = height_px / dpi. - Activate the new Origin graph, then unlock its default page ratio before setting physical dimensions. Keep graph elements independent of layer resizing:
op.lt_exec(
f"page.autoSize=0; page.kar=0; layer.fixed=1; "
f"page.width=page.resx*{width_in}; "
f"page.height=page.resy*{height_in};"
)
page.kar=0 is required. If it remains enabled, Origin preserves its default page ratio and an export specified by width will have the wrong height. Do not set page.width and page.height to pixel counts; they are printer dots.
- Set
layer.unit=1and apply the measured layer position as percentages. Set page dimensions before this step. For a frame(left, top, right, bottom)in awidth_px x height_pxtarget:
Left = 100 * left / width_px
Top = 100 * top / height_px
Width = 100 * (right - left) / width_px
Height = 100 * (bottom - top) / height_px
- Set all text categories separately. At minimum set Arial (or the inferred font) for x/y tick labels, x/y titles, legend, and free annotations. Do not rely on the active Origin theme to propagate fonts.
- Set curves with Origin's internal width conversion:
plot.set_cmd(f"-w {line_width_pt * 500}"). Set axis thickness separately withlayer.x.thicknessandlayer.y.thickness. Re-export after changinglayer.tickLorlayer.tickW; those properties are version-sensitive and should be verified visually rather than converted blindly. - When error bars must be pale while points are dark, draw two scatter layers per series: a pale Y-error layer with
-k 0(no symbol), then a dark circle-only layer withoutcolyerr. This avoids Origin coupling the error-bar and marker colors. - Build a custom legend when its square swatches or spacing differ from data symbols. Use independent legend symbols, no background, and scale attachment:
legend.text = (
r"\L(O Shape:Square, Interior:Solid, Style:s, Gap:0, "
r"Fill:#F47F1E, EdgeColor:#F47F1E, Size:10, EdgeWidth:0) Label"
)
op.lt_exec(
"legend.font=font(Arial); legend.fsize=11; legend.background=0; "
"legend.attach=2; doc -uw; "
"legend.x=layer.x.from+legend.dx/2+inset; "
"legend.y=layer.y.to-legend.dy/2-y_offset;"
)
Use one independent \L(O ...) entry per series with its sampled hex color. Tune legend.vgap, inset, and y_offset from the measured bitmap.
- Export at the target pixel width and verify the resulting pixel size. A width-only export must produce the expected height after page-ratio calibration:
from PIL import Image
graph.save_fig(str(output_png), type="png", width=width_px)
assert Image.open(output_png).size == (width_px, height_px)
If the dimensions do not match, stop and correct page.kar, physical page dimensions, or export settings before tuning fonts and lines.
Matplotlib Output
Provide a runnable starting snippet when the user wants Matplotlib or both targets:
import matplotlib.pyplot as plt
plt.rcParams.update({
"font.family": "Arial",
"axes.linewidth": 1.4,
"xtick.major.width": 1.2,
"ytick.major.width": 1.2,
"xtick.major.size": 4,
"ytick.major.size": 4,
"xtick.direction": "out",
"ytick.direction": "out",
"mathtext.fontset": "stix",
})
fig, ax = plt.subplots(figsize=(width_in, height_in))
fig.subplots_adjust(left=..., right=..., bottom=..., top=...)
ax.plot(x, y, color="#...", lw=...)
ax.set_xlabel(r"...", fontsize=...)
ax.set_ylabel(r"...", fontsize=...)
Map the detected layer rectangle to subplots_adjust:
left = frame_left / image_widthright = frame_right / image_widthbottom = 1 - frame_bottom / image_heighttop = 1 - frame_top / image_height
MATLAB Output
Provide a runnable starting snippet when the user wants MATLAB or all targets:
fig = figure('Units','inches', ...
'Position',[1 1 width_in height_in], ...
'Color','w');
ax = axes(fig, 'Units','normalized', ...
'Position',[left bottom width height], ...
'Box','on', ...
'LineWidth',1.4, ...
'TickDir','out', ...
'TickLength',[0.018 0.018], ...
'FontName','Arial', ...
'FontSize',11, ...
'XColor','k', ...
'YColor','k');
hold(ax, 'on');
plot(ax, x, y, 'Color',[r g b], 'LineWidth',1.5);
xlabel(ax, 'R + \DeltaR (\AA)', 'FontName','Arial', 'FontSize',16, 'Interpreter','tex');
ylabel(ax, '|\chi(R)| (\AA^{-4})', 'FontName','Arial', 'FontSize',16, 'Interpreter','tex');
plot(ax, [xguide xguide], [y0 y1], 'k--', 'LineWidth',1.2);
text(ax, xpos, ypos, 'label', 'FontName','Arial', 'FontSize',10, 'Interpreter','tex');
exportgraphics(fig, 'recreated_plot.png', 'Resolution',300);
Map the detected layer rectangle to MATLAB normalized axes position:
left = frame_left / image_widthbottom = 1 - frame_bottom / image_heightwidth = (frame_right - frame_left) / image_widthheight = (frame_bottom - frame_top) / image_height
MATLAB conversion notes:
- Convert hex/RGB 0-255 colors to MATLAB RGB triplets by dividing each channel by
255. - Use
axes(...,'Box','on')for a full four-sided frame. - Use endpoint
plot([x x],[y0 y1],...)instead ofxlinewhen dashed guide lines stop inside the plot. - Use the built-in
texinterpreter for common scientific labels such as\chi,\Delta, superscripts, subscripts, and\AA. Uselatexonly if the original clearly uses LaTeX-style math. - Prefer
exportgraphicsfor modern MATLAB. If matching older MATLAB, useprint(fig,'-dpng','-r300',filename).
Quality Checklist
Before answering, verify that the output includes:
- Image size and detected plot frame.
- For PDF input: source page, fixed DPI, PDF-point crop, source crop PNG, and vector-versus-raster provenance.
- For a direct reconstruction request: a runnable Python script, Python PNG, digitized CSVs, and, when automation is available, a verified
.opjuand Origin PNG. - Axis limits/ticks if readable.
- Line colors and line widths for all visible curves.
- Font family and approximate font sizes.
- Annotation and dashed guide-line styling.
- Origin, Matplotlib, and MATLAB settings unless the user requested only one or two targets.
- For OriginPro pixel calibration: unlocked page ratio, fixed element scaling, target DPI, verified export size, and separately calibrated text/axis/curve/error-bar/legend styling.
- For multi-panel PDF figures: one measured rectangle and editable layer per panel and inset.
- A clear approximation note and any uncertainty caused by low resolution, anti-aliasing, cropping, or missing source data.