77 | {text && text}
78 |
79 | {isDefined(numbers[0]) ? format(numbers[0]) : "-"}
80 |
81 |
82 | {isDefined(numbers[1]) ? format(numbers[1]) : "-"}
83 |
84 |
85 |
{children}
86 |
87 |
88 | {isDefined(numbers[2]) ? format(numbers[2]) : "-"}
89 |
90 |
91 | {isDefined(numbers[3]) ? format(numbers[3]) : "-"}
92 |
93 |
94 | );
95 |
96 | export default BoxSizing;
97 |
--------------------------------------------------------------------------------
/components/repl-layout.module.css:
--------------------------------------------------------------------------------
1 | .panelGrid {
2 | display: grid;
3 | height: 100%;
4 | padding: 10px;
5 | grid-template-columns: 1fr;
6 | grid-template-rows: auto 1fr auto;
7 | gap: 10px;
8 | }
9 |
10 | .headerControls {
11 | grid-row: 1;
12 | grid-column: 1;
13 |
14 | display: flex;
15 | width: 100%;
16 | font-family: monospace;
17 | font-size: 15px;
18 | justify-content: space-between;
19 | align-items: flex-start;
20 | }
21 |
22 | .preview {
23 | grid-row: 2;
24 | grid-column: 1;
25 |
26 | position: relative;
27 | }
28 |
29 | .footerControls {
30 | grid-row: 3;
31 | grid-column: 1;
32 |
33 | display: flex;
34 | width: 100%;
35 | font-family: monospace;
36 | font-size: 15px;
37 | justify-content: space-between;
38 | align-items: flex-start;
39 | }
40 |
41 | .buttons {
42 | display: flex;
43 | gap: 3px;
44 | flex-wrap: wrap;
45 | align-items: center;
46 | justify-content: center;
47 | }
48 |
49 | .selectWithInfo {
50 | display: flex;
51 | flex-direction: column;
52 | width: max-content;
53 | }
54 |
55 | .infoTime {
56 | font-size: 12px;
57 | color: #101010;
58 | }
59 |
60 | .resizeHandleOuter {
61 | flex: 0 0 1px;
62 | position: relative;
63 | outline: none;
64 |
65 | --background-color: #d2d3d3;
66 | }
67 |
68 | .resizeHandleInner {
69 | width: 100%;
70 | height: 100%;
71 | background-color: var(--background-color);
72 | transition: background-color 0.2s linear;
73 | }
74 |
75 | .resizeHandleOuter[data-resize-handle-active],
76 | .resizeHandleOuter[data-resize-handle-active] > .resizeHandleInner:hover {
77 | --background-color: rgba(0, 0, 0, 0.6);
78 | }
79 |
80 | .resizeHandleInner:hover {
81 | --background-color: rgba(100, 100, 100, 0.7);
82 | }
83 |
84 | .resizeHandleHoverArea {
85 | position: absolute;
86 | z-index: 1;
87 | background-color: transparent;
88 | }
89 |
90 | .resizeHandleOuter[data-panel-group-direction="horizontal"]
91 | > .resizeHandleHoverArea {
92 | width: 7px;
93 | inset: 0 -3px;
94 | }
95 |
96 | .resizeHandleOuter[data-panel-group-direction="vertical"]
97 | > .resizeHandleHoverArea {
98 | height: 7px;
99 | inset: -3px 0;
100 | }
101 |
102 | .scrollBox {
103 | display: flex;
104 | flex-direction: column;
105 | overflow: auto;
106 | width: 100%;
107 | height: 100%;
108 | }
109 |
110 | .debugFont {
111 | font-family: monospace;
112 | font-size: 12px;
113 | }
114 |
115 | .emptyDebugger {
116 | width: 100%;
117 | height: 100%;
118 |
119 | display: flex;
120 |
121 | align-items: center;
122 | justify-content: center;
123 |
124 | font-family: monospace;
125 | font-size: 14px;
126 |
127 | padding: 10px;
128 | }
129 |
130 | .debugInfo {
131 | font-family: monospace;
132 | font-size: 12px;
133 |
134 | display: flex;
135 | flex-direction: column;
136 | padding: 10px;
137 | gap: 10px;
138 |
139 | min-height: 100%;
140 | }
141 |
142 | .nodeStyles {
143 | flex-grow: 1;
144 | }
145 |
146 | .nodeStyles > pre {
147 | margin: 0;
148 | }
149 |
150 | .boxInfo {
151 | min-width: max-content;
152 | display: flex;
153 | align-items: center;
154 | justify-content: center;
155 | }
156 |
157 | .errorContainer {
158 | position: absolute;
159 | inset: 0;
160 | width: 100%;
161 |
162 | padding: 10px 20px;
163 |
164 | z-index: 1;
165 | background-color: rgba(255, 255, 255, 0.6);
166 | backdrop-filter: blur(12px);
167 |
168 | display: flex;
169 | flex-direction: column;
170 | gap: 20px;
171 | }
172 |
173 | .replError {
174 | font-family: iaw-mono-var, SF Mono, SFMono-Regular, ui-monospace, Menlo,
175 | Consolas, monospace;
176 | overflow: auto;
177 | color: #ff3737;
178 | font-size: 13px;
179 | }
180 |
181 | .replError > pre {
182 | margin: 0;
183 | white-space: pre-wrap;
184 | }
185 |
186 | .errorActions {
187 | display: flex;
188 | gap: 10px;
189 | }
190 |
191 | .githubLink {
192 | display: flex;
193 | justify-content: center;
194 | }
195 |
--------------------------------------------------------------------------------
/components/repl-layout.js:
--------------------------------------------------------------------------------
1 | import { PanelResizeHandle } from "react-resizable-panels";
2 | import * as styles from "./repl-layout.module.css";
3 |
4 | const Buttons = ({ children }) => (
5 | .+)/;
62 | const Error = ({ error, actions = [] }) => {
63 | const message =
64 | typeof error === "string"
65 | ? error
66 | : error.stack || error.message;
67 | const { groups } = errorRegexp.exec(message) ?? {
68 | groups: { errors: message },
69 | };
70 |
71 | const showActions = error?.fatal;
72 |
73 | return (
74 |
75 |
76 |
77 | {groups.errors
78 | .trim()
79 | .split(",")
80 | .map((part) => part.trim())
81 | .join("\n")}
82 |
83 |
84 | {showActions && (
85 |
86 | {actions.map(([text, action]) => (
87 |
90 | ))}
91 |
92 | )}
93 |
94 | );
95 | };
96 |
97 | const GithubButton = () => (
98 |
103 |
109 |
110 | );
111 |
112 | export {
113 | Buttons,
114 | Version,
115 | ResizeHandle,
116 | ScrollBox,
117 | DebugFont,
118 | EmptyDebugger,
119 | DebugInfo,
120 | Styles,
121 | BoxInfo,
122 | PreviewPanel,
123 | HeaderControls,
124 | FooterControls,
125 | Preview,
126 | Error,
127 | GithubButton,
128 | };
129 |
--------------------------------------------------------------------------------
/pages/api/og.js:
--------------------------------------------------------------------------------
1 | import "ses";
2 | import { createCanvas, Image, Path2D } from "@napi-rs/canvas";
3 | import * as pdfjs from "pdfjs-dist/legacy/build/pdf";
4 | import "pdfjs-dist/legacy/build/pdf.worker";
5 | import { readFile } from "node:fs/promises";
6 |
7 | import * as React from "react";
8 | import { renderToBuffer } from "@react-pdf/renderer";
9 | import {
10 | toModule,
11 | LayoutProvider,
12 | serializeLayout,
13 | } from "../../worker/to-module";
14 |
15 | import { decompress, gzDecompress } from "../../code/lz";
16 | import { code } from "../../code/default-example";
17 |
18 | const templatePromise = readFile(new URL("./g-template.png", import.meta.url));
19 |
20 | const getTemplate = async () => {
21 | const template = new Image();
22 | template.src = await templatePromise;
23 |
24 | return template;
25 | };
26 |
27 | const evaluate = async (code) => {
28 | const { namespace } = await toModule(code);
29 |
30 | if (!("default" in namespace)) {
31 | throw new Error("The default export is not a React PDF Component");
32 | }
33 |
34 | let layout = null;
35 |
36 | const element = React.createElement(
37 | LayoutProvider,
38 | { fn: (info) => (layout = info._INTERNAL__LAYOUT__DATA_) },
39 | React.createElement(namespace.default)
40 | );
41 | const buffer = await renderToBuffer(element);
42 |
43 | return {
44 | buffer,
45 | layout: serializeLayout(layout),
46 | };
47 | };
48 |
49 | const NodeCanvasFactory = {
50 | create(width, height) {
51 | const canvas = createCanvas(width, height);
52 | const context = canvas.getContext("2d");
53 | return {
54 | canvas,
55 | context,
56 | };
57 | },
58 |
59 | reset(canvasAndContext, width, height) {
60 | canvasAndContext.canvas.width = width;
61 | canvasAndContext.canvas.height = height;
62 | },
63 |
64 | destroy(canvasAndContext) {
65 | canvasAndContext.canvas = null;
66 | canvasAndContext.context = null;
67 | },
68 | };
69 |
70 | async function getCanvas(pagePromise, { height, width }) {
71 | const page = await pagePromise;
72 | let viewport = page.getViewport({ scale: 1.0 });
73 | viewport = page.getViewport({
74 | scale: Math.max(height / viewport.height, width / viewport.width),
75 | });
76 |
77 | const canvasFactory = NodeCanvasFactory;
78 | const { canvas, context } = canvasFactory.create(
79 | viewport.width,
80 | viewport.height
81 | );
82 | const renderContext = {
83 | canvasContext: context,
84 | viewport,
85 | canvasFactory,
86 | };
87 |
88 | const renderTask = page.render(renderContext);
89 | await renderTask.promise;
90 |
91 | return { canvas, viewport };
92 | }
93 |
94 | export default async function GET(req, res) {
95 | // cors preflight
96 | if (req.method?.toUpperCase() === "OPTIONS") {
97 | return res.status(200).send("ok");
98 | }
99 |
100 | const canvas = createCanvas(1600, 800);
101 |
102 | const { cp_code, gz_code } = req.query;
103 | const ctx = canvas.getContext("2d");
104 | ctx.fillStyle = "white";
105 | ctx.fillRect(0, 0, 1600, 900);
106 | ctx.drawImage(await getTemplate(), 0, 0);
107 |
108 | try {
109 | const pdf = await Promise.race([
110 | evaluate(
111 | (() => {
112 | if (gz_code) return gzDecompress(gz_code);
113 | if (cp_code) return decompress(cp_code);
114 | return code;
115 | })()
116 | ),
117 | new Promise((_, reject) =>
118 | setTimeout(() => reject(Error("Timeout")), 8_500)
119 | ),
120 | ]);
121 |
122 | const document = await pdfjs.getDocument({
123 | data: pdf.buffer,
124 | verbosity: 0,
125 | }).promise;
126 |
127 | const { canvas: pdfCanvas, viewport } = await getCanvas(
128 | document.getPage(1),
129 | {
130 | width: 1400,
131 | height: 800 - 104,
132 | }
133 | );
134 | ctx.save();
135 | ctx.fillStyle = "#e2e2e2";
136 | ctx.shadowColor = "rgba(0, 0, 0, .15)";
137 | ctx.shadowBlur = 200;
138 | ctx.shadowOffsetY = 300;
139 | ctx.fillRect(100 - 1, 104 - 1, viewport.width + 2, viewport.height + 2);
140 | ctx.drawImage(pdfCanvas, 100, 104);
141 | ctx.restore();
142 | } catch (error) {
143 | console.log(error);
144 | // error state image
145 | ctx.save();
146 | ctx.fillStyle = "#F5D0D0";
147 | ctx.shadowColor = "rgba(0, 0, 0, .15)";
148 | ctx.shadowBlur = 200;
149 | ctx.shadowOffsetY = 300;
150 | ctx.fillRect(100, 104, 1400, 810);
151 | ctx.restore();
152 |
153 | ctx.save();
154 | ctx.strokeStyle = "rgba(255, 0, 0, 0.5)";
155 | ctx.lineWidth = 10;
156 | ctx.strokeRect(100 + 5, 104 + 5, 1400 - 10, 800);
157 | ctx.restore();
158 |
159 | ctx.save();
160 | ctx.translate(758, 469);
161 | const crossPath = new Path2D(
162 | "M76.2 83.6 0 7.3 7.3 0l76.3 76.2-7.4 7.4Zm-68.8 0L0 76 76.1 0l7.5 7.4L7.4 83.6Z"
163 | );
164 | ctx.fillStyle = "rgba(255, 0, 0, 0.5)";
165 | ctx.fill(crossPath);
166 | ctx.restore();
167 | }
168 |
169 | res.setHeader("Cache-Control", "public, max-age=2592000, immutable");
170 | res.setHeader("content-type", "image/jpeg");
171 | return res.send(canvas.toBuffer("image/jpeg"));
172 | }
173 |
--------------------------------------------------------------------------------
/components/elements-tree.js:
--------------------------------------------------------------------------------
1 | import { useAtom } from "jotai/react";
2 | import {
3 | forwardRef,
4 | useCallback,
5 | useMemo,
6 | useEffect,
7 | useState,
8 | useRef,
9 | } from "react";
10 |
11 | import scrollIntoView from "scroll-into-view-if-needed";
12 |
13 | import {
14 | selectedAtom,
15 | hoverAtom,
16 | hoverPathAtom,
17 | layoutAtom,
18 | } from "../state/debugger";
19 | import { page as pageNumberAtom } from "../state/page";
20 |
21 | import styles from "./elements-tree.module.css";
22 |
23 | const cn = (...styles) => styles.filter((v) => v).join(" ");
24 |
25 | const OpenTag = forwardRef(function OpenTag(
26 | { children, className, indent, expand, expanded, ...props },
27 | ref
28 | ) {
29 | return (
30 |
31 | expand((v) => !v)}>
32 | {expanded ? "▼" : "▶"}
33 |
34 |
35 |
36 | {children}
37 |
38 |
39 | );
40 | });
41 |
42 | const Leaf = ({ node, indent }) => {
43 | const attrs = node.props
44 | ? Object.entries(node.props)
45 | .map(([key, value]) => `${key}="${value}"`)
46 | .join(" ")
47 | : null;
48 |
49 | const tagWithAttrs = [node.type, attrs].filter(Boolean).join(" ");
50 |
51 | const elementRef = useRef();
52 | const [, select] = useAtom(selectedAtom);
53 | const [hover, setHover] = useAtom(hoverAtom);
54 | const [selected] = useAtom(selectedAtom);
55 | const [hoverPath] = useAtom(hoverPathAtom);
56 | const [layout] = useAtom(layoutAtom);
57 | const [page, setPage] = useAtom(pageNumberAtom);
58 |
59 | const [iexpanded, iexpand] = useState(() => hoverPath.includes(node._id));
60 |
61 | const expanded = useMemo(() => {
62 | if (node.type === "PAGE") {
63 | return layout.children[page - 1] === node && iexpanded;
64 | } else {
65 | return iexpanded;
66 | }
67 | }, [iexpanded, layout.children, node, page]);
68 |
69 | const expand = useCallback(
70 | (value) => {
71 | if (node.type === "PAGE") {
72 | const index = layout.children.indexOf(node);
73 | setPage(index + 1);
74 | return iexpand(index + 1 !== page ? true : value);
75 | } else {
76 | return iexpand(value);
77 | }
78 | },
79 | [layout.children, node, page, setPage]
80 | );
81 |
82 | useEffect(() => {
83 | if (hoverPath.includes(node._id)) {
84 | expand(true);
85 | }
86 | }, [expand, hoverPath, node._id]);
87 |
88 | useEffect(() => {
89 | if (selected?._id === node._id && elementRef.current) {
90 | scrollIntoView(elementRef.current, {
91 | behavior: "smooth",
92 | block: "center",
93 | scrollMode: 'if-needed',
94 | });
95 | }
96 | }, [node._id, selected?._id]);
97 |
98 | const getClassName = (node) =>
99 | cn(
100 | styles.line,
101 | hover === node._id && styles.hover,
102 | selected?._id === node._id && styles.selected
103 | );
104 |
105 | const props = {
106 | onMouseEnter: () => setHover({ id: node._id, pathable: false }),
107 | onMouseLeave: () => setHover(null),
108 | onClick: () => select(node),
109 | };
110 |
111 | if (node.type === "TEXT_INSTANCE" && node.parent && node.parent.lines) {
112 | return node.parent.lines.map((line, index) => (
113 | setHover({ id: node.parent._id, pathable: false })}
119 | onMouseLeave={() => setHover(null)}
120 | onClick={() => select(node.parent)}
121 | >
122 | {line.string}
123 |
124 | ));
125 | } else if (node.children && node.children.length > 0 && !expanded) {
126 | return (
127 | {`<${tagWithAttrs}>...${node.type}>`}
136 | );
137 | } else if (node.children && node.children.length > 0 && expanded) {
138 | return (
139 | <>
140 | {`<${tagWithAttrs}>`}
149 |
150 | {`${node.type}>`}
155 | >
156 | );
157 | } else {
158 | return (
159 | {`<${tagWithAttrs} />`}
165 | );
166 | }
167 | };
168 |
169 | const Tree = ({ nodes, indent = 0 }) =>
170 | nodes.map((child, index) => (
171 |
172 | ));
173 |
174 | export default Tree;
175 | export { Tree, Leaf };
176 |
--------------------------------------------------------------------------------
/components/viewer.js:
--------------------------------------------------------------------------------
1 | import * as pdfjs from "pdfjs-dist";
2 | import workerSrc from "pdfjs-dist/build/pdf.worker.min.js";
3 | import { useEffect, useRef } from "react";
4 | import useResizeObserver from "@react-hook/resize-observer";
5 | import { createSingleton, useEventCallback, useSetState } from "../hooks";
6 | import TreeCore from "./debug-tree";
7 | import { loader } from "./viewer.module.css";
8 |
9 | pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
10 |
11 | const useWorker = createSingleton(
12 | () => new pdfjs.PDFWorker({ name: "pdfjs-worker" }),
13 | (worker) => worker.destroy()
14 | );
15 |
16 | const client = (fn) => typeof window !== "undefined" && fn();
17 |
18 | let canvases = [];
19 | const getCanvas = (viewport) => {
20 | const canvas = canvases.length
21 | ? canvases.pop()
22 | : document.createElement("canvas");
23 |
24 | if (viewport.width !== canvas.width) {
25 | canvas.width = viewport.width;
26 | }
27 |
28 | if (viewport.height !== canvas.height) {
29 | canvas.height = viewport.height;
30 | }
31 |
32 | canvas.style.display = "block";
33 |
34 | return canvas;
35 | };
36 |
37 | const freeCanvas = (canvas) => {
38 | canvases.push(canvas);
39 | };
40 |
41 | const Canvas = ({ className, style, canvas, before, after }) => {
42 | const ref = useRef();
43 | const beforeCallback = useEventCallback(before);
44 | const afterCallback = useEventCallback(after);
45 |
46 | useEffect(() => {
47 | if (canvas) {
48 | ref.current.append(canvas);
49 | beforeCallback(canvas);
50 | return () => {
51 | canvas.remove();
52 | afterCallback(canvas);
53 | };
54 | }
55 | }, [canvas, ref, beforeCallback, afterCallback]);
56 |
57 | return ;
58 | };
59 |
60 | const Viewer = ({ page: pageNumber, url, isDebugging, layout, onParse }) => {
61 | const worker = useWorker();
62 |
63 | const [state, set] = useSetState({
64 | document: null,
65 | size: null,
66 | canvas: null,
67 | });
68 |
69 | const onParseCallback = useEventCallback(onParse);
70 | useEffect(() => {
71 | if (state.document) {
72 | onParseCallback({ pagesCount: state.document.numPages });
73 | }
74 | }, [state.document, onParseCallback]);
75 |
76 | useEffect(() => {
77 | const getDocument = ({ signal }) =>
78 | Promise.resolve(signal.aborted)
79 | .then((aborted) => {
80 | if (aborted) throw Error("aborted");
81 | const task = pdfjs.getDocument({ url, worker });
82 | signal.addEventListener("abort", () => task.destroy());
83 | return task.promise;
84 | })
85 | .then((document) => {
86 | if (!signal.aborted) {
87 | set({ document });
88 | }
89 | })
90 | .catch((error) => {
91 | if (!signal.aborted) {
92 | throw error;
93 | }
94 | });
95 |
96 | if (url) {
97 | const controller = new AbortController();
98 | getDocument({ signal: controller.signal });
99 |
100 | return () => {
101 | controller.abort();
102 | };
103 | } else {
104 | set({ document: null });
105 | }
106 | }, [set, url, worker]);
107 |
108 | useEffect(() => {
109 | const getRenderedPage = ({ pageNumber, signal }) =>
110 | Promise.resolve(signal.aborted)
111 | .then((aborted) => {
112 | if (aborted) throw Error("aborted");
113 | return state.document.getPage(pageNumber);
114 | })
115 | .then((page) => {
116 | let viewport = page.getViewport({
117 | scale: 1 / window.devicePixelRatio,
118 | });
119 | const size = state.size;
120 | const scale = size.width / viewport.width;
121 |
122 | viewport = page.getViewport({ scale });
123 |
124 | const canvas = getCanvas(viewport);
125 | const context = canvas.getContext("2d");
126 |
127 | const task = page.render({ canvasContext: context, viewport });
128 |
129 | return task.promise.then(() => canvas);
130 | })
131 | .then((canvas) => {
132 | if (!signal.aborted) {
133 | set({ canvas });
134 | }
135 | })
136 | .catch((error) => {
137 | if (!signal.aborted) {
138 | throw error;
139 | }
140 | });
141 |
142 | if (
143 | state.document &&
144 | state.size &&
145 | pageNumber &&
146 | state.document.numPages >= pageNumber
147 | ) {
148 | const controller = new AbortController();
149 | getRenderedPage({ pageNumber, signal: controller.signal });
150 |
151 | return () => {
152 | controller.abort();
153 | };
154 | }
155 | }, [pageNumber, set, state.document, state.size]);
156 |
157 | const blockRef = useRef();
158 | useResizeObserver(blockRef, (entry) => {
159 | const { width = 0, height = 0 } = entry.contentRect;
160 | // borders
161 | set({ size: { width: width - 2, height: height - 2 } });
162 | });
163 |
164 | return (
165 |
171 |
180 | {state.document && pageNumber && (
181 |
193 | )}
194 |
195 | {isDebugging && layout && (
196 |
197 | )}
198 |
199 |
200 | {state.document && !pageNumber && (
201 |
209 | Blank PDF Document
210 |
211 | )}
212 |
213 | {state.size && !state.document && (
214 |
223 | )}
224 |
225 | );
226 | };
227 |
228 | export default Viewer;
229 |
--------------------------------------------------------------------------------
/components/debug-tree.js:
--------------------------------------------------------------------------------
1 | import { useAtom } from "jotai/react";
2 | import { useEffect, useMemo, useRef } from "react";
3 |
4 | import { hoverAtom, selectedAtom, layoutAtom } from "../state/debugger";
5 | import { page as pageNumberAtom } from "../state/page";
6 |
7 | const scale = (box, dpr) =>
8 | dpr > 1
9 | ? Object.fromEntries(
10 | Object.entries(box).map(([key, value]) => [key, value * dpr])
11 | )
12 | : box;
13 |
14 | const trimNegative = (value) => (value < 0 ? 0 : value);
15 | const getNegative = (value) => (value > 0 ? 0 : value);
16 |
17 | const BoxOnCanvas = ({ box }) => {
18 | const ref = useRef();
19 |
20 | useEffect(() => {
21 | const ctx = ref.current.getContext("2d");
22 | const dpr = window.devicePixelRatio || 1;
23 |
24 | const {
25 | borderBottomWidth,
26 | borderLeftWidth,
27 | borderRightWidth,
28 | borderTopWidth,
29 |
30 | marginBottom,
31 | marginLeft,
32 | marginRight,
33 | marginTop,
34 |
35 | paddingBottom,
36 | paddingLeft,
37 | paddingRight,
38 | paddingTop,
39 |
40 | right,
41 | top,
42 | bottom,
43 | left,
44 |
45 | width,
46 | height,
47 | } = scale(box, dpr);
48 |
49 | const canvasWidth =
50 | width + trimNegative(marginLeft) + trimNegative(marginRight);
51 | const canvasHeight =
52 | height + trimNegative(marginBottom) + trimNegative(marginTop);
53 |
54 | ctx.canvas.width = canvasWidth;
55 | ctx.canvas.height = canvasHeight;
56 |
57 | // margin
58 | if (
59 | marginBottom !== 0 ||
60 | marginLeft !== 0 ||
61 | marginRight !== 0 ||
62 | marginTop !== 0
63 | ) {
64 | ctx.fillStyle = "rgb(248 204 158)";
65 | ctx.fillRect(
66 | -getNegative(marginLeft),
67 | -getNegative(marginTop),
68 | width + marginLeft + marginRight,
69 | height + marginBottom + marginTop
70 | );
71 | }
72 |
73 | if (
74 | borderBottomWidth !== 0 ||
75 | borderLeftWidth !== 0 ||
76 | borderRightWidth !== 0 ||
77 | borderTopWidth !== 0
78 | ) {
79 | // border
80 | ctx.fillStyle = "rgb(250 220 160)";
81 | ctx.fillRect(
82 | trimNegative(marginLeft),
83 | trimNegative(marginTop),
84 | width,
85 | height
86 | );
87 | }
88 |
89 | if (
90 | paddingBottom !== 0 ||
91 | paddingLeft !== 0 ||
92 | paddingRight !== 0 ||
93 | paddingTop !== 0
94 | ) {
95 | // padding
96 | ctx.fillStyle = "rgb(196 207 140)";
97 | ctx.fillRect(
98 | trimNegative(marginLeft) + borderLeftWidth,
99 | trimNegative(marginTop) + borderTopWidth,
100 | width - borderLeftWidth - borderRightWidth,
101 | height - borderTopWidth - borderBottomWidth
102 | );
103 | }
104 |
105 | // body
106 | ctx.fillStyle = "rgb(172 201 255)";
107 | ctx.fillRect(
108 | trimNegative(marginLeft) + paddingLeft + borderLeftWidth,
109 | trimNegative(marginTop) + paddingTop + borderTopWidth,
110 | width - paddingLeft - paddingRight - borderLeftWidth - borderRightWidth,
111 | height - paddingBottom - paddingTop - borderTopWidth - borderBottomWidth
112 | );
113 |
114 | if (dpr > 1) {
115 | ctx.canvas.style.transformOrigin = "0 0";
116 | ctx.canvas.style.transform = `scale(${1 / dpr})`;
117 | }
118 | ctx.canvas.style.opacity = 0.75;
119 | ctx.canvas.style.position = "absolute";
120 | ctx.canvas.style.top = `${-trimNegative(box.marginTop)}px`;
121 | ctx.canvas.style.left = `${-trimNegative(box.marginLeft)}px`;
122 | ctx.canvas.style.pointerEvents = "none";
123 | }, [box]);
124 |
125 | return ;
126 | };
127 |
128 | const Box = ({ box, children, active, minPresenceAhead, ...props }) => (
129 |
141 | {active &&
}
142 | {minPresenceAhead && active && (
143 |
153 | )}
154 | {children}
155 |
156 | );
157 |
158 | const DebugLeaf = ({ node }) => {
159 | const [hover] = useAtom(hoverAtom);
160 |
161 | const [layout] = useAtom(layoutAtom);
162 | const [page] = useAtom(pageNumberAtom);
163 |
164 | const skip = useMemo(() => {
165 | if (node.type === "PAGE") {
166 | return layout.children[page - 1] !== node;
167 | } else {
168 | return false;
169 | }
170 | }, [layout.children, node, page]);
171 |
172 | const props = {
173 | "data-id": node._id,
174 | box: node.box,
175 | active: node._id === hover,
176 | minPresenceAhead: node.props?.minPresenceAhead
177 | ? Number.parseFloat(node.props?.minPresenceAhead)
178 | : null,
179 | };
180 |
181 | if (skip || node.type === "TEXT_INSTANCE") {
182 | return null;
183 | } else if (node.type === "DOCUMENT") {
184 | return ;
185 | } else if (node.children && node.children.length > 0) {
186 | return (
187 |
188 |
189 |
190 | );
191 | } else {
192 | return ;
193 | }
194 | };
195 |
196 | const DebugTree = ({ nodes }) =>
197 | nodes.map((node, index) => );
198 |
199 | const TreeCore = ({ nodes, size }) => {
200 | const [, select] = useAtom(selectedAtom);
201 | const [, hover] = useAtom(hoverAtom);
202 | const [page] = useAtom(pageNumberAtom);
203 |
204 | if (
205 | !nodes[0].children.length ||
206 | !page ||
207 | !nodes[0].children[page - 1] ||
208 | !size
209 | )
210 | return null;
211 |
212 | const { width } = nodes[0].children[page - 1].box;
213 | const scale = size.width / width;
214 |
215 | return (
216 | select(e.target.dataset.id)}
218 | onMouseEnter={(e) => hover(e.target.dataset.id)}
219 | onMouseMove={(e) => hover(e.target.dataset.id)}
220 | onMouseLeave={() => hover(null)}
221 | style={{
222 | position: "absolute",
223 | top: 1, // border
224 | left: 1, // border
225 | transform: `scale(${scale})`,
226 | }}
227 | >
228 |
229 |
230 | );
231 | };
232 |
233 | export { DebugTree, DebugLeaf, Box };
234 | export default TreeCore;
235 |
--------------------------------------------------------------------------------
/worker/to-module.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3 | import * as tailwind from "react-pdf-tailwind";
4 | import * as rpGlobals from "@react-pdf/renderer";
5 | import { StaticModuleRecord } from "./better-static-module-record.mjs";
6 |
7 | // q, to quote strings in error messages.
8 | const q = JSON.stringify;
9 |
10 | // makeStaticRetriever mocks the behavior of a real retriever, like HTTP fetch
11 | // or a file system fetch function, using an in memory map of sources to file
12 | // text.
13 | export const makeStaticRetriever = (sources) => {
14 | return async (moduleLocation) => {
15 | const string = sources[moduleLocation];
16 | if (string === undefined) {
17 | throw new ReferenceError(
18 | `Cannot retrieve module at location ${q(moduleLocation)}.`
19 | );
20 | }
21 | return string;
22 | };
23 | };
24 |
25 | // makeImporter combines a locator and retriever to make an importHook suitable
26 | // for a Compartment.
27 | export const makeImporter = (locate, retrieve) => async (moduleSpecifier) => {
28 | const moduleLocation = locate(moduleSpecifier);
29 | const string = await retrieve(moduleLocation);
30 | return new StaticModuleRecord(string, moduleLocation, {
31 | jsx: true,
32 | });
33 | };
34 |
35 | const createVirtualModuleFromVariable = (name, exports, options = {}) => {
36 | const { jsx } = options;
37 | const exportKeys = Object.keys(exports);
38 | const hasDefault = exportKeys.some((_export) => _export === "default");
39 | const exportsList = exportKeys.filter((_export) => _export !== "default");
40 | const declarations = exportsList.map((_, index) => `__v$${index}$__`);
41 | const declarationString = declarations
42 | .map((id, index) => `${id} = ${exportsList[index]}`)
43 | .join(",");
44 | const exportString = declarations
45 | .map((id, index) => `${id} as ${exportsList[index]}`)
46 | .join(",");
47 |
48 | const moduleRecord = new StaticModuleRecord(
49 | [
50 | `const ${declarationString}`,
51 | `export { ${exportString} }`,
52 | hasDefault ? `export default __v_default__` : null,
53 | ]
54 | .filter(Boolean)
55 | .join(";"),
56 |
57 | name,
58 | { jsx }
59 | );
60 |
61 | let globals = exports;
62 | if (hasDefault) {
63 | const { default: __v_default__, ...rest } = exports;
64 | globals = { __v_default__, ...rest };
65 | }
66 |
67 | const compartment = new Compartment(
68 | globals,
69 | {},
70 | {
71 | name,
72 | resolveHook: (spec) => spec,
73 | importHook: (moduleSpecifier) => {
74 | if (moduleSpecifier !== name) {
75 | throw new ReferenceError(
76 | `Cannot retrieve module at location ${q(moduleSpecifier)}.`
77 | );
78 | }
79 | return moduleRecord;
80 | },
81 | }
82 | );
83 |
84 | return compartment.module(name);
85 | };
86 |
87 | const tailwindModule = createVirtualModuleFromVariable(
88 | "react-pdf-tailwind",
89 | tailwind
90 | );
91 | const reactModule = createVirtualModuleFromVariable("react", React);
92 | const reactRuntimeModule = createVirtualModuleFromVariable(
93 | "react/jsx-runtime",
94 | {
95 | jsx,
96 | jsxs,
97 | Fragment,
98 | }
99 | );
100 |
101 | const LayoutContext = React.createContext({
102 | createOnRender: (originalCallback) => (info) => {
103 | console.log(originalCallback, info);
104 | },
105 | });
106 |
107 | const createDocumentWithCallback = (ReactPDF) =>
108 | function DocumentWithCallback(props) {
109 | const context = React.useContext(LayoutContext);
110 |
111 | return React.createElement(ReactPDF.Document, {
112 | ...props,
113 | onRender: context.createOnRender(props.onRender),
114 | });
115 | };
116 |
117 | export const LayoutProvider = (props) => {
118 | const createOnRender = (originalCallback) => {
119 | if (!originalCallback) return props.fn;
120 |
121 | const handler = (info) => {
122 | const { layout, ...other } = info;
123 | props.fn({ layout });
124 | originalCallback(other);
125 | };
126 |
127 | handler._originalFunction = originalCallback;
128 |
129 | return handler;
130 | };
131 | return React.createElement(
132 | LayoutContext.Provider,
133 | { value: { createOnRender } },
134 | props.children
135 | );
136 | };
137 |
138 | const reactPdfModule = createVirtualModuleFromVariable("@react-pdf/renderer", {
139 | ...rpGlobals,
140 | Document: createDocumentWithCallback(rpGlobals),
141 | });
142 |
143 | const isRelative = (spec) =>
144 | spec.startsWith("./") ||
145 | spec.startsWith("../") ||
146 | spec === "." ||
147 | spec === "..";
148 |
149 | const locate = (moduleSpecifier) => moduleSpecifier;
150 | const resolveHook = (spec, referrer) => {
151 | if (isRelative(spec)) {
152 | return new URL(spec, referrer).toString();
153 | }
154 |
155 | return spec;
156 | };
157 |
158 | export const toModule = async (code) => {
159 | const retrieve = makeStaticRetriever({
160 | "file://internal/code.js": code,
161 | });
162 | const importHook = makeImporter(locate, retrieve);
163 |
164 | const compartment = new Compartment(
165 | {
166 | console,
167 | fetch: (...a) => fetch(...a),
168 | Headers,
169 | Request,
170 | Response,
171 | },
172 | {
173 | react: reactModule,
174 | "react/jsx-runtime": reactRuntimeModule,
175 | "@react-pdf/renderer": reactPdfModule,
176 | "react-pdf-tailwind": tailwindModule,
177 | },
178 | {
179 | name: "repl",
180 | resolveHook,
181 | importHook,
182 | }
183 | );
184 |
185 | return compartment.import("file://internal/code.js");
186 | };
187 |
188 | const fitInto = (string, size) => {
189 | if (string.length <= size) return string;
190 | const halfOfSize = Math.floor((size - 3) / 2);
191 | return [
192 | string.substring(0, halfOfSize),
193 | string.substring(string.length - halfOfSize, string.length),
194 | ].join("...");
195 | };
196 |
197 | const fnName = (fn) => fn.name ?? "anonymous function";
198 |
199 | const serializeProps = (props) => {
200 | if (!props) return;
201 |
202 | return Object.fromEntries(
203 | Object.entries(props)
204 | .map(([key, value]) => {
205 | if (key === "onRender") {
206 | return value._originalFunction
207 | ? [key, fnName(value._originalFunction)]
208 | : false;
209 | }
210 |
211 | if (value === null) return [key, "null"];
212 | if (value === undefined) return [key, "undefined"];
213 |
214 | if (Array.isArray(value)) return [key, `[${value.join(", ")}]`];
215 |
216 | switch (typeof value) {
217 | case "number":
218 | case "boolean":
219 | return [key, value.toString()];
220 | case "string":
221 | return [key, fitInto(value.trim(), 100)];
222 | case "function":
223 | return [key, fnName(value)];
224 | default:
225 | return [key, typeof value];
226 | }
227 | })
228 | .filter(Boolean)
229 | );
230 | };
231 |
232 | export const serializeLayout = (layout) => {
233 | const serializeNode = (node) => {
234 | const sNode = {
235 | box: node.box,
236 | style: node.style,
237 | type: node.type,
238 | props: serializeProps(node.props),
239 | lines: node?.lines?.map((line) => ({ string: line.string })),
240 | children: [],
241 | };
242 |
243 | if (node.children) {
244 | for (let child of node.children) {
245 | if (child.type !== "TEXT_INSTANCE") {
246 | sNode.children.push(serializeNode(child));
247 | }
248 | }
249 | }
250 |
251 | return sNode;
252 | };
253 |
254 | return layout && serializeNode(layout);
255 | };
256 |
--------------------------------------------------------------------------------
/worker/better-static-module-record.mjs:
--------------------------------------------------------------------------------
1 | import { Parser } from "acorn";
2 | import { generate } from "astring";
3 | import { walk } from "estree-walker";
4 | import { buildJsx } from "estree-util-build-jsx";
5 | import jsx from "acorn-jsx";
6 |
7 | const parser = Parser.extend(jsx());
8 |
9 | const checkDefault = (value) => ("__default" === value ? "default" : value);
10 |
11 | const createProgramBody = (body, staticImports, staticExports) => [
12 | {
13 | type: "ExpressionStatement",
14 | expression: {
15 | type: "ArrowFunctionExpression",
16 | id: null,
17 | expression: false,
18 | generator: false,
19 | async: false,
20 | params: [
21 | {
22 | type: "ObjectPattern",
23 | properties: [
24 | {
25 | type: "Property",
26 | key: {
27 | type: "Identifier",
28 | name: "imports",
29 | },
30 | value: {
31 | type: "Identifier",
32 | name: "__imports",
33 | },
34 | kind: "init",
35 | },
36 | {
37 | type: "Property",
38 | key: {
39 | type: "Identifier",
40 | name: "onceVar",
41 | },
42 | value: {
43 | type: "Identifier",
44 | name: "__once",
45 | },
46 | kind: "init",
47 | },
48 | ],
49 | },
50 | ],
51 | body: {
52 | type: "BlockStatement",
53 | body: [
54 | staticImports.length && {
55 | type: "VariableDeclaration",
56 | declarations: staticImports
57 | .flatMap(({ importMap }) => Object.values(importMap))
58 | .map((name) => ({
59 | type: "VariableDeclarator",
60 | id: { type: "Identifier", name },
61 | })),
62 | kind: "let",
63 | },
64 | {
65 | type: "ExpressionStatement",
66 |
67 | expression: {
68 | type: "CallExpression",
69 |
70 | callee: {
71 | type: "Identifier",
72 |
73 | name: "__imports",
74 | },
75 | arguments: [
76 | {
77 | type: "ArrayExpression",
78 | elements: staticImports.map(({ source, importMap }) => ({
79 | type: "ArrayExpression",
80 | elements: [
81 | {
82 | type: "Literal",
83 | value: source,
84 | },
85 | {
86 | type: "ArrayExpression",
87 | elements: Object.entries(importMap).map(
88 | ([imported, local]) => ({
89 | type: "ArrayExpression",
90 | elements: [
91 | {
92 | type: "Literal",
93 | value: imported,
94 | },
95 | {
96 | type: "ArrayExpression",
97 | elements: [
98 | {
99 | type: "ArrowFunctionExpression",
100 | params: [
101 | {
102 | type: "Identifier",
103 | name: "__a",
104 | },
105 | ],
106 | body: {
107 | type: "AssignmentExpression",
108 | operator: "=",
109 | left: {
110 | type: "Identifier",
111 | name: local,
112 | },
113 | right: {
114 | type: "Identifier",
115 | name: "__a",
116 | },
117 | },
118 | },
119 | ],
120 | },
121 | ],
122 | })
123 | ),
124 | },
125 | ],
126 | })),
127 | },
128 | ],
129 |
130 | optional: false,
131 | },
132 | },
133 | ...body,
134 | ...(staticExports.length
135 | ? staticExports.map((staticExport) => ({
136 | type: "ExpressionStatement",
137 | expression: {
138 | type: "CallExpression",
139 | callee: {
140 | type: "MemberExpression",
141 | object: {
142 | type: "Identifier",
143 | name: "__once",
144 | },
145 | property: {
146 | type: "Identifier",
147 | name: checkDefault(Object.values(staticExport)[0]),
148 | },
149 | computed: false,
150 | optional: false,
151 | },
152 | arguments: [
153 | {
154 | type: "Identifier",
155 | name: Object.values(staticExport)[0],
156 | },
157 | ],
158 | optional: false,
159 | },
160 | }))
161 | : []),
162 | ].filter(Boolean),
163 | },
164 | },
165 | },
166 | ];
167 |
168 | const getImports = (node) => {
169 | const source = node.source.value;
170 |
171 | let importMap;
172 | switch (node.specifiers[0].type) {
173 | case "ImportDefaultSpecifier":
174 | importMap = {
175 | default: node.specifiers[0].local.name,
176 | };
177 | break;
178 |
179 | case "ImportSpecifier":
180 | importMap = Object.fromEntries(
181 | node.specifiers.map((specifier) => [
182 | specifier.imported.name,
183 | specifier.local.name,
184 | ])
185 | );
186 | break;
187 |
188 | case "ImportNamespaceSpecifier":
189 | importMap = {
190 | "*": node.specifiers[0].local.name,
191 | };
192 | break;
193 |
194 | default:
195 | throw ReferenceError("unknown import type");
196 | }
197 |
198 | return { source, importMap };
199 | };
200 |
201 | const exportDefaultReplacement = (value) => ({
202 | type: "VariableDeclaration",
203 | declarations: [
204 | {
205 | type: "VariableDeclarator",
206 | id: {
207 | type: "ObjectPattern",
208 | properties: [
209 | {
210 | type: "Property",
211 | key: {
212 | type: "Identifier",
213 | name: "default",
214 | },
215 | value: {
216 | type: "Identifier",
217 | name: "__default",
218 | },
219 | kind: "init",
220 | },
221 | ],
222 | },
223 | init: {
224 | type: "ObjectExpression",
225 | properties: [
226 | {
227 | type: "Property",
228 | key: {
229 | type: "Identifier",
230 | name: "default",
231 | },
232 | value,
233 | kind: "init",
234 | },
235 | ],
236 | },
237 | },
238 | ],
239 | kind: "const",
240 | });
241 |
242 | export class StaticModuleRecord {
243 | constructor(source, location, { jsx } = {}) {
244 | this.jsx = jsx;
245 | this.source = source;
246 | this.location = location;
247 | }
248 |
249 | init() {
250 | const ast = parser.parse(this.source, {
251 | sourceType: "module",
252 | ecmaVersion: 2020,
253 | });
254 |
255 | if (this.jsx) {
256 | buildJsx(ast, { runtime: "automatic" });
257 | }
258 |
259 | const staticImports = [];
260 | const staticExports = [];
261 | let flatStaticExports = [];
262 |
263 | walk(ast, {
264 | enter(node) {
265 | if (node.type === "ImportDeclaration") {
266 | staticImports.push(getImports(node));
267 | this.remove();
268 | } else if (node.type === "ExportNamedDeclaration") {
269 | if (!node.declaration) {
270 | staticExports.push(
271 | Object.fromEntries(
272 | node.specifiers.map((specifier) => [
273 | specifier.exported.name,
274 | specifier.local.name,
275 | ])
276 | )
277 | );
278 |
279 | this.remove();
280 | } else {
281 | switch (node.declaration.type) {
282 | case "ClassDeclaration":
283 | case "FunctionDeclaration": {
284 | const name = node.declaration.id.name;
285 | staticExports.push({ [name]: name });
286 | this.replace(node.declaration);
287 | break;
288 | }
289 |
290 | case "VariableDeclaration": {
291 | const exports = Object.fromEntries(
292 | node.declaration.declarations.map((declarator) => [
293 | declarator.id.name,
294 | declarator.id.name,
295 | ])
296 | );
297 | staticExports.push(exports);
298 |
299 | this.replace(node.declaration);
300 | break;
301 | }
302 |
303 | default:
304 | throw ReferenceError("unknown import type");
305 | }
306 | }
307 | } else if (node.type === "ExportDefaultDeclaration") {
308 | staticExports.push({ default: "__default" });
309 |
310 | this.replace(exportDefaultReplacement(node.declaration));
311 | } else if (node.type !== "Program") {
312 | this.skip();
313 | }
314 | },
315 |
316 | leave(node, parent, prop, index) {
317 | if (node.type === "Program") {
318 | flatStaticExports = staticExports.flatMap((staticExport) =>
319 | Object.entries(staticExport).map((pair) =>
320 | Object.fromEntries([pair])
321 | )
322 | );
323 | node.body = createProgramBody(
324 | node.body,
325 | staticImports,
326 | flatStaticExports
327 | );
328 | }
329 | },
330 | });
331 |
332 | this._imports = staticImports.map(({ source }) => source);
333 | this._exports = flatStaticExports.flatMap((map) => Object.keys(map));
334 | this._exportMap = Object.fromEntries(
335 | flatStaticExports.flatMap((singleExport) =>
336 | Object.entries(singleExport).map(([key, value]) => [
337 | key,
338 | [checkDefault(value)],
339 | ])
340 | )
341 | );
342 |
343 | this._syncProgram = generate(ast);
344 | }
345 |
346 | get imports() {
347 | if (!this._imports) this.init();
348 |
349 | return this._imports;
350 | }
351 |
352 | get exports() {
353 | if (!this._exports) this.init();
354 |
355 | return this._exports;
356 | }
357 |
358 | get __syncModuleProgram__() {
359 | if (!this._syncProgram) this.init();
360 |
361 | return this._syncProgram;
362 | }
363 |
364 | get __liveExportMap__() {
365 | return {};
366 | }
367 |
368 | get __fixedExportMap__() {
369 | if (!this._exportMap) this.init();
370 |
371 | return this._exportMap;
372 | }
373 | }
374 |
--------------------------------------------------------------------------------
/code/default-example.js:
--------------------------------------------------------------------------------
1 | const small = `import { Document, Page, Text } from '@react-pdf/renderer';
2 |
3 | const center = {
4 | alignItems: 'center',
5 | justifyContent: 'center'
6 | }
7 |
8 | export default () => (
9 |
10 |
11 |
12 | HELLO WORLD !!!
13 |
14 |
15 |
16 | );
17 | `;
18 |
19 | const big = `import { StyleSheet, Document, Page, View, Text } from '@react-pdf/renderer';
20 |
21 | const styles = StyleSheet.create({
22 | body: {
23 | flexGrow: 1,
24 | },
25 | row: {
26 | flexGrow: 1,
27 | flexDirection: "row",
28 | },
29 | text: {
30 | width: "60%",
31 | textAlign: "justify",
32 | fontSize: 50,
33 | padding: 20,
34 | },
35 | fill1: {
36 | width: "40%",
37 | backgroundColor: "#e14427",
38 | },
39 | });
40 |
41 | export default () => (
42 |
43 |
44 |
45 |
46 |
47 | Lorem ipsum dolor sita met cons ecte tur ad ip
48 | Lorem ipsum dolor sita met cons ecte tur ad ip
49 |
50 |
51 |
52 |
53 |
54 |
55 | Lorem ipsum dolor sita met cons ecte tur ad ip
56 | Lorem ipsum dolor sita met cons ecte tur ad ip
57 |
58 |
59 |
60 |
61 | Lorem ipsum dolor sita met cons ecte tur ad ip
62 | Lorem ipsum dolor sita met cons ecte tur ad ip
63 |
64 |
65 |
66 |
67 |
68 |
69 | Lorem ipsum dolor sita met cons ecte tur ad ip
70 | Lorem ipsum dolor sita met cons ecte tur ad ip
71 |
72 |
73 |
74 |
75 |
76 |
77 | );
78 | `;
79 |
80 | const resume = `import {
81 | Link,
82 | Text,
83 | Font,
84 | Page,
85 | View,
86 | Image,
87 | Document,
88 | StyleSheet,
89 | } from "@react-pdf/renderer";
90 |
91 | const headerStyles = StyleSheet.create({
92 | container: {
93 | flexDirection: "row",
94 | borderBottomWidth: 2,
95 | borderBottomColor: "#112131",
96 | borderBottomStyle: "solid",
97 | alignItems: "stretch",
98 | },
99 | detailColumn: {
100 | flexDirection: "column",
101 | flexGrow: 9,
102 | textTransform: "uppercase",
103 | },
104 | linkColumn: {
105 | flexDirection: "column",
106 | flexGrow: 2,
107 | alignSelf: "flex-end",
108 | justifySelf: "flex-end",
109 | },
110 | name: {
111 | fontSize: 24,
112 | fontFamily: "Lato Bold",
113 | },
114 | subtitle: {
115 | fontSize: 10,
116 | justifySelf: "flex-end",
117 | fontFamily: "Lato",
118 | },
119 | link: {
120 | fontFamily: "Lato",
121 | fontSize: 10,
122 | color: "black",
123 | textDecoration: "none",
124 | alignSelf: "flex-end",
125 | justifySelf: "flex-end",
126 | },
127 | });
128 |
129 | const Header = () => (
130 |
131 |
132 | Luke Skywalker
133 | Jedi Master
134 |
135 |
136 |
137 | luke@theforce.com
138 |
139 |
140 |
141 | );
142 |
143 | const titleStyles = StyleSheet.create({
144 | title: {
145 | fontFamily: "Lato Bold",
146 | fontSize: 14,
147 | marginBottom: 10,
148 | textTransform: "uppercase",
149 | },
150 | });
151 |
152 | const Title = ({ children }) => (
153 | {children}
154 | );
155 |
156 | const listStyles = StyleSheet.create({
157 | item: {
158 | flexDirection: "row",
159 | marginBottom: 5,
160 | },
161 | bulletPoint: {
162 | width: 10,
163 | fontSize: 10,
164 | },
165 | itemContent: {
166 | flex: 1,
167 | fontSize: 10,
168 | fontFamily: "Lato",
169 | },
170 | });
171 |
172 | const List = ({ children }) => children;
173 |
174 | const Item = ({ children }) => (
175 |
176 | •
177 | {children}
178 |
179 | );
180 |
181 | const skilsStyles = StyleSheet.create({
182 | title: {
183 | fontFamily: "Lato Bold",
184 | fontSize: 11,
185 | marginBottom: 10,
186 | },
187 | skills: {
188 | fontFamily: "Lato",
189 | fontSize: 10,
190 | marginBottom: 10,
191 | },
192 | });
193 |
194 | const SkillEntry = ({ name, skills }) => (
195 |
196 | {name}
197 |
198 | {skills.map((skill, i) => (
199 | - {skill}
200 | ))}
201 |
202 |
203 | );
204 |
205 | const Skills = () => (
206 |
207 | Skills
208 |
216 |
217 | );
218 |
219 | const educationStyles = StyleSheet.create({
220 | container: {
221 | marginBottom: 10,
222 | },
223 | school: {
224 | fontFamily: "Lato Bold",
225 | fontSize: 10,
226 | },
227 | degree: {
228 | fontFamily: "Lato",
229 | fontSize: 10,
230 | },
231 | candidate: {
232 | fontFamily: "Lato Italic",
233 | fontSize: 10,
234 | },
235 | });
236 |
237 | const Education = () => (
238 |
239 | Education
240 | Jedi Academy
241 | Jedi Master
242 | A long, long time ago
243 |
244 | );
245 |
246 | const expStyles = StyleSheet.create({
247 | container: {
248 | flex: 1,
249 | paddingTop: 30,
250 | paddingLeft: 15,
251 | "@media max-width: 400": {
252 | paddingTop: 10,
253 | paddingLeft: 0,
254 | },
255 | },
256 | entryContainer: {
257 | marginBottom: 10,
258 | },
259 | date: {
260 | fontSize: 11,
261 | fontFamily: "Lato Italic",
262 | },
263 | detailContainer: {
264 | flexDirection: "row",
265 | },
266 | detailLeftColumn: {
267 | flexDirection: "column",
268 | marginLeft: 10,
269 | marginRight: 10,
270 | },
271 | detailRightColumn: {
272 | flexDirection: "column",
273 | flexGrow: 9,
274 | },
275 | bulletPoint: {
276 | fontSize: 10,
277 | },
278 | details: {
279 | fontSize: 10,
280 | fontFamily: "Lato",
281 | },
282 | headerContainer: {
283 | flexDirection: "row",
284 | marginBottom: 10,
285 | },
286 | leftColumn: {
287 | flexDirection: "column",
288 | flexGrow: 9,
289 | },
290 | rightColumn: {
291 | flexDirection: "column",
292 | flexGrow: 1,
293 | alignItems: "flex-end",
294 | justifySelf: "flex-end",
295 | },
296 | title: {
297 | fontSize: 11,
298 | color: "black",
299 | textDecoration: "none",
300 | fontFamily: "Lato Bold",
301 | },
302 | });
303 |
304 | const ExperienceEntry = ({ company, details, position, date }) => {
305 | const title = \`\${company} | \${position}\`;
306 | return (
307 |
308 |
309 |
310 | {title}
311 |
312 |
313 | {date}
314 |
315 |
316 |
317 | {details.map((detail, i) => (
318 | -
319 | {detail}
320 |
321 | ))}
322 |
323 |
324 | );
325 | };
326 |
327 | const experienceData = [
328 | {
329 | company: "Jedi Temple, Coruseant",
330 | date: "A long time ago...",
331 | details: [
332 | "Started a new Jedi Temple in order to train the next generation of Jedi Masters",
333 | "Discovered and trained a new generation of Jedi Knights, which he recruited from within the New Republic",
334 | "Communicates with decesased Jedi Masters such as Anakin Skywalker, Yoda, Obi-Wan Kenobi in order to learn the secrets of the Jedi Order",
335 | ],
336 | position: "Head Jedi Master",
337 | },
338 | {
339 | company: "Rebel Alliance",
340 | date: "A long time ago...",
341 | details: [
342 | "Lead legions of troops into battle while demonstrating bravery, competence and honor",
343 | "Created complicated battle plans in conjunction with other Rebel leaders in order to ensure the greatest chance of success",
344 | "Defeated Darth Vader in single-combat, and convinced him to betray his mentor, the Emperor",
345 | ],
346 | position: "General",
347 | },
348 | {
349 | company: "Rebel Alliance",
350 | date: "A long time ago...",
351 | details: [
352 | "Destroyed the Death Star by using the force to find its only weakness and delivering a torpedo into the center of the ship",
353 | "Commanded of squadron of X-Wings into battle",
354 | "Defeated an enemy AT-AT single handedly after his ship was destroyed",
355 | "Awarded a medal for valor and bravery in battle for his successful destruction of the Death Star",
356 | ],
357 | position: "Lieutenant Commander",
358 | },
359 | {
360 | company: "Tatooine Moisture Refinery",
361 | date: "A long time ago...",
362 | details: [
363 | "Replaced damaged power converters",
364 | "Performed menial labor thoughout the farm in order to ensure its continued operation",
365 | ],
366 | position: "Moisture Farmer",
367 | },
368 | ];
369 |
370 | const Experience = () => (
371 |
372 | Experience
373 | {experienceData.map(({ company, date, details, position }) => (
374 |
381 | ))}
382 |
383 | );
384 |
385 | const resumeStyles = StyleSheet.create({
386 | page: {
387 | padding: 30,
388 | },
389 | container: {
390 | flex: 1,
391 | flexDirection: "row",
392 | "@media max-width: 400": {
393 | flexDirection: "column",
394 | },
395 | },
396 | image: {
397 | marginBottom: 10,
398 | },
399 | leftColumn: {
400 | flexDirection: "column",
401 | width: 170,
402 | paddingTop: 30,
403 | paddingRight: 15,
404 | "@media max-width: 400": {
405 | width: "100%",
406 | paddingRight: 0,
407 | },
408 | "@media orientation: landscape": {
409 | width: 200,
410 | },
411 | },
412 | footer: {
413 | fontSize: 12,
414 | fontFamily: "Lato Bold",
415 | textAlign: "center",
416 | marginTop: 15,
417 | paddingTop: 5,
418 | borderWidth: 3,
419 | borderColor: "gray",
420 | borderStyle: "dashed",
421 | "@media orientation: landscape": {
422 | marginTop: 10,
423 | },
424 | },
425 | });
426 |
427 | Font.register({
428 | family: "Open Sans",
429 | src: "https://fonts.gstatic.com/s/opensans/v17/mem8YaGs126MiZpBA-UFVZ0e.ttf",
430 | });
431 |
432 | Font.register({
433 | family: "Lato",
434 | src: "https://fonts.gstatic.com/s/lato/v16/S6uyw4BMUTPHjx4wWw.ttf",
435 | });
436 |
437 | Font.register({
438 | family: "Lato Italic",
439 | src: "https://fonts.gstatic.com/s/lato/v16/S6u8w4BMUTPHjxsAXC-v.ttf",
440 | });
441 |
442 | Font.register({
443 | family: "Lato Bold",
444 | src: "https://fonts.gstatic.com/s/lato/v16/S6u9w4BMUTPHh6UVSwiPHA.ttf",
445 | });
446 |
447 | const Resume = (props) => (
448 |
449 |
450 |
451 |
452 |
456 |
457 |
458 |
459 |
460 |
461 |
462 | This IS the candidate you are looking for
463 |
464 |
465 | );
466 |
467 | export default () => (
468 |
474 |
475 |
476 |
477 |
478 | );
479 | `;
480 |
481 | export { resume as code };
482 |
--------------------------------------------------------------------------------
/app/repl.js:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { lazy, useEffect, useReducer, useRef, useState } from "react";
4 | import Editor from "@monaco-editor/react";
5 | import useConstant from "use-constant";
6 | import { useAtom } from "jotai/react";
7 | import { log } from "next-axiom/dist/logger";
8 |
9 | import { Panel as ResizablePanel, PanelGroup } from "react-resizable-panels";
10 |
11 | import { createSingleton, useSetState } from "../hooks";
12 | import { Worker } from "../worker";
13 | import Tree from "../components/elements-tree";
14 | import BoxSizing from "../components/box-sizing";
15 | import {
16 | Buttons,
17 | Version,
18 | ResizeHandle,
19 | ScrollBox,
20 | DebugFont,
21 | DebugInfo,
22 | Styles,
23 | BoxInfo,
24 | PreviewPanel,
25 | HeaderControls,
26 | FooterControls,
27 | EmptyDebugger,
28 | Preview,
29 | Error,
30 | GithubButton,
31 | } from "../components/repl-layout";
32 | import { loader } from "../components/viewer.module.css";
33 | import {
34 | page,
35 | pagesCount,
36 | canDecrease,
37 | canIncrease,
38 | increase,
39 | decrease,
40 | } from "../state/page";
41 |
42 | import { layoutAtom, selectedAtom } from "../state/debugger";
43 |
44 | import { decompress, gzCompress, gzDecompress } from "../code/lz";
45 | import { code as defCode } from "../code/default-example";
46 |
47 | const Viewer = lazy(() => import("../components/viewer"));
48 |
49 | const useWorker = createSingleton(
50 | () => (typeof document !== "undefined" ? new Worker() : null),
51 | (worker) => worker?.terminate()
52 | );
53 |
54 | function useMediaQuery(query) {
55 | const getMatches = (query) => {
56 | if (typeof window !== "undefined") {
57 | return window.matchMedia(query).matches;
58 | }
59 | return false;
60 | };
61 |
62 | const [matches, setMatches] = useState(() => getMatches(query));
63 |
64 | useEffect(() => {
65 | function handleChange() {
66 | setMatches(getMatches(query));
67 | }
68 |
69 | const matchMedia = window.matchMedia(query);
70 |
71 | // Triggered at the first client-side load and if query changes
72 | handleChange();
73 |
74 | matchMedia.addEventListener("change", handleChange);
75 |
76 | return () => {
77 | matchMedia.removeEventListener("change", handleChange);
78 | };
79 | }, [query]);
80 |
81 | return matches;
82 | }
83 |
84 | const ClientOnly = ({ children }) => {
85 | const [isClient, set] = useState(false);
86 |
87 | useEffect(() => set(true), []);
88 |
89 | return isClient ? children : null;
90 | };
91 |
92 | const Loader = () => ;
93 |
94 | const addId = (node, parent, prefix, postfix) => {
95 | if (parent) node.parent = parent;
96 | node._id = [prefix, node.type, postfix].filter((v) => v).join("__");
97 |
98 | if (node.children)
99 | node.children.forEach((child, index) => {
100 | addId(child, node, node._id, index + 1);
101 | });
102 |
103 | return node;
104 | };
105 |
106 | const createLink = (options) => {
107 | const link = new URL(window.location);
108 |
109 | link.searchParams.set("gz_code", gzCompress(options.code));
110 |
111 | if (options.modules) {
112 | link.searchParams.set("modules", options.modules);
113 | }
114 |
115 | return link.toString();
116 | };
117 |
118 | const Repl = () => {
119 | const urlParams = useConstant(() => {
120 | if (typeof window === "undefined") return {};
121 |
122 | return Object.fromEntries(
123 | Array.from(new URLSearchParams(window.location.search).entries()).map(
124 | ([key, value]) => {
125 | if (key.startsWith("cp_")) {
126 | return [key.slice(3), decompress(value)];
127 | }
128 | if (key.startsWith("gz_")) {
129 | return [key.slice(3), gzDecompress(value)];
130 | }
131 |
132 | return [key, value];
133 | }
134 | )
135 | );
136 | });
137 |
138 | const isMobile = useMediaQuery("(max-width: 600px)");
139 |
140 | const [options, updateOptions] = useSetState(() => ({
141 | modules: urlParams.code ? Boolean(urlParams.modules) : true,
142 | }));
143 |
144 | const timeout = useRef(20_000);
145 |
146 | const [state, update] = useSetState(() => ({
147 | url: null,
148 | version: null,
149 | time: null,
150 | error: null,
151 | isDebuggingSupported: options.modules,
152 | isDebugging: true,
153 | isEditing: true,
154 | }));
155 |
156 | const [isReady, setReady] = useState(false);
157 | const [code, setCode] = useState(() => urlParams.code ?? defCode);
158 | const [v, forceRender] = useReducer((v) => !v);
159 |
160 | const [pageV] = useAtom(page);
161 | const [, setPagesCount] = useAtom(pagesCount);
162 | const [canDecreaseV] = useAtom(canDecrease);
163 | const [, decreaseS] = useAtom(decrease);
164 | const [canIncreaseV] = useAtom(canIncrease);
165 | const [, increaseS] = useAtom(increase);
166 |
167 | const [layout, setLayout] = useAtom(layoutAtom);
168 | const [selectedNode] = useAtom(selectedAtom);
169 |
170 | const pdf = useWorker();
171 |
172 | const debuggerAPI = useRef();
173 | const editorPanelAPI = useRef();
174 |
175 | useEffect(() => {
176 | if (isReady) {
177 | pdf.call("version").then(({ version, isDebuggingSupported }) =>
178 | update({
179 | version,
180 | isDebuggingSupported: options.modules && isDebuggingSupported,
181 | })
182 | );
183 | } else {
184 | pdf.call("init").then(() => setReady(true));
185 | }
186 | }, [pdf, update, isReady, options.modules]);
187 |
188 | useEffect(() => {
189 | if (isReady) {
190 | const startTime = Date.now();
191 | pdf
192 | .call("evaluate", {
193 | code,
194 | options: { modules: options.modules },
195 | timeout: timeout.current,
196 | })
197 | .then(({ url, layout }) => {
198 | if (layout) {
199 | setLayout(addId(layout));
200 | } else {
201 | setLayout(null);
202 | }
203 | update({ url, time: Date.now() - startTime, error: null });
204 | })
205 | .catch((error) => {
206 | if (error.fatal) {
207 | log.error(error.message, {
208 | link: createLink({ code, ...options }),
209 | });
210 | }
211 | update({ time: Date.now() - startTime, error });
212 | });
213 | }
214 | }, [pdf, code, update, isReady, setLayout, options, v]);
215 |
216 | const editorPanel = (
217 | update({ isEditing: !collapsed })}
223 | >
224 | }
226 | language="javascript"
227 | value={code}
228 | onChange={(newCode) => {
229 | setCode(newCode ?? "");
230 | }}
231 | beforeMount={(_monaco) => {
232 | _monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
233 | allowNonTsExtensions: true,
234 | checkJs: true,
235 | allowJs: true,
236 | noLib: true,
237 | jsx: "react",
238 | });
239 | _monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions(
240 | { noSemanticValidation: true }
241 | );
242 | }}
243 | options={{
244 | wordWrap: "on",
245 | tabSize: 2,
246 | minimap: {
247 | enabled: false,
248 | },
249 | contextmenu: false,
250 | }}
251 | />
252 |
253 | );
254 |
255 | const viewerPanel = (
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 | {pageV && (
264 |
265 |
268 |
275 | page:
276 |
277 | {pageV}
278 |
279 |
280 |
283 |
284 | )}
285 |
286 |
287 |
296 |
297 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 | setPagesCount(pagesCount)}
317 | />
318 |
319 |
320 | {state.error && (
321 | {
327 | pdf.start();
328 | forceRender();
329 | },
330 | ],
331 |
332 | [
333 | "increase timeout and restart",
334 | () => {
335 | pdf.start();
336 | timeout.current = timeout.current * 2;
337 | forceRender();
338 | },
339 | ],
340 | ]}
341 | />
342 | )}
343 |
344 |
345 |
346 |
360 |
374 |
375 |
376 |
377 |
378 |
379 |
380 | update({ isDebugging: !collapsed })}
384 | ref={debuggerAPI}
385 | >
386 | {state.isDebuggingSupported ? (
387 |
391 |
392 | {layout && (
393 |
394 |
395 |
396 |
397 |
398 | )}
399 |
400 |
401 |
402 |
403 |
404 | {selectedNode && selectedNode.style && (
405 |
406 |
407 | {Object.entries(selectedNode.style)
408 | .map(([key, value]) => `${key}: ${value}`)
409 | .join("\n")}
410 |
411 |
412 | )}
413 |
414 | {selectedNode && (
415 |
416 |
417 |
418 | )}
419 |
420 |
421 |
422 |
423 | ) : (
424 | {`Debugger doesn't supported by this @react-pdf/renderer version`}
425 | )}
426 |
427 |
428 |
429 | );
430 |
431 | return (
432 |
433 | {isMobile ? (
434 |
439 | {viewerPanel}
440 |
441 | {editorPanel}
442 |
443 | ) : (
444 |
449 | {editorPanel}
450 |
451 | {viewerPanel}
452 |
453 | )}
454 |
455 | );
456 | };
457 |
458 | export default Repl;
459 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-pdf-repl
2 |
3 | REPL for `@react-pdf/renderer` with interactive debugger
4 |
5 | ## Examples
6 |
7 | | [![][context_example_image]][context_example] react context usage| [![][emoji_example_image]][emoji_example] emojis |
8 | |:---:|:---:|
9 | | [![][presence_example_image]][presence_example] minPresenceAhead| [![][dimensions_example_image]][dimensions_example] margin padding and border |
10 | | [![][watermark_example_image]][watermark_example] watermark| [![][tailwind_example_image]][tailwind_example] react-pdf and tailwind |
11 | | [![][image_fallback_example_image]][image_fallback_example] image fallback| [![][exif_bug_example_image]][exif_bug_example] exif bug |
12 |
13 | [Create new doc](https://react-pdf-repl.vercel.app/new)
14 |
15 | [context_example]: https://react-pdf-repl.vercel.app/?gz_code=eJzlVWtP2zAU_d5fcRdtUiqVpOU11LXVgME2adMQMKSBkDCJ01pK4sh2oSzKf9-9zqNpeWj7tmkVJY7vw-eee3wrkkwqA3kH4Mw8xPxsxrnp4dsHGcwTntr1CZtyel4Ifk_Pc76w-8eS7AVESibgvFecBWYjCyNf8TTkiivnXUdUB0CAZsMPMYSiYa6X68OZiEOM6UEQy5QfxZxOhjqxzYupOnSep_hUaMOVS5gjloj4YQjOt4yncMZS7RAwrQLcmxmT6aHvRximvak2zIjAC2Tia1-iv0Z3_27w1k94sveDfdSDzd2v4jI72N_4fnxx2eeeMRHmK7p4diBTbUATRxrGLbK8sjAL51aGCIZWABkLQ5FOhzDoE6RiJc85u43r-jHbCjduvu6IHi4S2LCkxU8EUXRhPAEXc4_a6bwTJe8E0g93LJ7zcZ7X_sXEAhtRG8tKyHgrFTofSGNkgmB71capmM4MvlNYXh9djHwKpjwj_8lDJ50l9lN5v4q8gkz8lB4NNPRbCsJtZ8Z0ACIC95V17YKZKcx7pJRUroMcVykyJTMwsiIMm5yhklLj2HjFzVyllqtH9UcxX3wQigdGyBRVg9mdhiuAvBanl7DMXfagXPZAoNIXVFblDysarr3KQocl1isbc41sVDHdogTWkIugi4bGQx7HTymgLQAKtDnqsqrMjQidHdShvRv0oZqHcNOHPrzOKVfx5qa2le0_lxmJYWXvC49Ms1kQ5kpQNA9WRGI3OsuKSBN8YQdByCM2jw24S_D1qGn1BHt__PhqLyVME8mSMHb2tx2QSmA8ow6OnZiloQ5Yxp06YXlrPbqdTV_XVMBiMU0_G55oPJHo2cBxoUxbCfU9K5s4zq92e4B_m3s92OrT97rlis4o__Y77lAnJ4ORb59PmDafN209b9p-3rTz2DTy13D9nzg_4T8J91LFYfkrw8BQc_8R-C_Q_OeV_VXwX6D59ytbg1_9VjVXv5mztKZRUs6qeg6V8-oX3nOE4Q&modules=true
16 |
17 | [context_example_image]: https://react-pdf-repl.vercel.app/api/og?gz_code=eJzlVWtP2zAU_d5fcRdtUiqVpOU11LXVgME2adMQMKSBkDCJ01pK4sh2oSzKf9-9zqNpeWj7tmkVJY7vw-eee3wrkkwqA3kH4Mw8xPxsxrnp4dsHGcwTntr1CZtyel4Ifk_Pc76w-8eS7AVESibgvFecBWYjCyNf8TTkiivnXUdUB0CAZsMPMYSiYa6X68OZiEOM6UEQy5QfxZxOhjqxzYupOnSep_hUaMOVS5gjloj4YQjOt4yncMZS7RAwrQLcmxmT6aHvRximvak2zIjAC2Tia1-iv0Z3_27w1k94sveDfdSDzd2v4jI72N_4fnxx2eeeMRHmK7p4diBTbUATRxrGLbK8sjAL51aGCIZWABkLQ5FOhzDoE6RiJc85u43r-jHbCjduvu6IHi4S2LCkxU8EUXRhPAEXc4_a6bwTJe8E0g93LJ7zcZ7X_sXEAhtRG8tKyHgrFTofSGNkgmB71capmM4MvlNYXh9djHwKpjwj_8lDJ50l9lN5v4q8gkz8lB4NNPRbCsJtZ8Z0ACIC95V17YKZKcx7pJRUroMcVykyJTMwsiIMm5yhklLj2HjFzVyllqtH9UcxX3wQigdGyBRVg9mdhiuAvBanl7DMXfagXPZAoNIXVFblDysarr3KQocl1isbc41sVDHdogTWkIugi4bGQx7HTymgLQAKtDnqsqrMjQidHdShvRv0oZqHcNOHPrzOKVfx5qa2le0_lxmJYWXvC49Ms1kQ5kpQNA9WRGI3OsuKSBN8YQdByCM2jw24S_D1qGn1BHt__PhqLyVME8mSMHb2tx2QSmA8ow6OnZiloQ5Yxp06YXlrPbqdTV_XVMBiMU0_G55oPJHo2cBxoUxbCfU9K5s4zq92e4B_m3s92OrT97rlis4o__Y77lAnJ4ORb59PmDafN209b9p-3rTz2DTy13D9nzg_4T8J91LFYfkrw8BQc_8R-C_Q_OeV_VXwX6D59ytbg1_9VjVXv5mztKZRUs6qeg6V8-oX3nOE4Q&modules=true
18 |
19 |
20 | [emoji_example]: https://react-pdf-repl.vercel.app/?gz_code=eJy1Uk1Lw0AQvfdXDEEhhTTbUuwhpkXxAwQPQsWrxuwkWUl2w-7GVkP-hHjqpSdB_50_wV2baqGCXrwMs2_m7b43s6wohdRQw6ng2oNLnJt4EaXowRXDmQfHIq4K5BoaSKQowDmQGMW6V9KESOQUJUpnv9OxfF9iypRGeVKIOzYVlYzRrTsAtxXLTWcAbiwodmE8MSDATaZ1qQJCYsr9O0UxZ_fS56hJmhGVVZTxlCR5ZZ6vWA_tpb2KM3sHiZRCrchObU--FudihvIoUuh2m-sh9Uue3nidpmuk4fzTI8UkqnINrn0fXKMgXJtbyQmtb1DsEcfO4chZgQa2g2hzAKUfchzX9RcAkBjrU8MKYDjyNvAMWZrpAJxBv7_rbFbKiFpvAQz2NuEoZyk_01goQ4qNLjPa73rTtOnkCwrtvibvy8UrvC-f3rbD4jUknz1blJ-62_A75fllHf6T8mdhf7YfErvLdtvErtvmIfn-B-a_fACFSBSn&modules=true
21 |
22 | [emoji_example_image]: https://react-pdf-repl.vercel.app/api/og?gz_code=eJy1Uk1Lw0AQvfdXDEEhhTTbUuwhpkXxAwQPQsWrxuwkWUl2w-7GVkP-hHjqpSdB_50_wV2baqGCXrwMs2_m7b43s6wohdRQw6ng2oNLnJt4EaXowRXDmQfHIq4K5BoaSKQowDmQGMW6V9KESOQUJUpnv9OxfF9iypRGeVKIOzYVlYzRrTsAtxXLTWcAbiwodmE8MSDATaZ1qQJCYsr9O0UxZ_fS56hJmhGVVZTxlCR5ZZ6vWA_tpb2KM3sHiZRCrchObU--FudihvIoUuh2m-sh9Uue3nidpmuk4fzTI8UkqnINrn0fXKMgXJtbyQmtb1DsEcfO4chZgQa2g2hzAKUfchzX9RcAkBjrU8MKYDjyNvAMWZrpAJxBv7_rbFbKiFpvAQz2NuEoZyk_01goQ4qNLjPa73rTtOnkCwrtvibvy8UrvC-f3rbD4jUknz1blJ-62_A75fllHf6T8mdhf7YfErvLdtvErtvmIfn-B-a_fACFSBSn&modules=true
23 |
24 | [presence_example]: https://react-pdf-repl.vercel.app/?gz_code=eJy9U01v00AQvedXjNxLKrl2PmiJQhzRFgVxQFRqqUQRh8UeJ4u8Xmd33A8s_3dmYydOCIgb8sHrmeeZeW_eSlVoQ1DBHT6TDzdiiT7cS3zy4Z2OS4U5Rxc6J6ghNVqB99agiOmsSNLQYJ6gQeO96fUcJjC4lJbQ9KseQCqUzF6m4H0qMIdbkVvP57A1McdWRIWdhmHKv9lgaUmQjINYq9CGmvGW4eHj8HWoUE2-iPd2OLr4KB-Kq8uzz4v7hwEGRCnXq0-5d6xzS3AtTAIR9CtYoeC5fOA4oRv9FKI59Ln5zFEDSy8ZRlUFTzKh1RRGg4EPhUgSmS-ncA51PWcso50omxOAkvmNQYt5jJeuflSNz-s2t63XfjJ1bnwrfyKXfuXvom2HK02kFffpMsR97gxTTrXhjFcWBZpYWPQ6TKwzbTh3MplMduG6GWHeflYN8yY4C934e0w63t18wwu_q4wj93iOf9VqV3dVZqETb95ziuPzxjYJpqLMCPqdwFvXHDZbHHuhU9mZroMf7AEsTxlVX8duQ7ymb_WW6maTO3GONgDwo7Qk05frhge3jvnFXvX3MCKTy_wDobJ_ytfb_c53oZkz2V6BRu_IW2GWaTgZenu5VsHIu8cMHnVWFiTs9sA9QdC6RAezuC4FlQbQGG0g19-lDfgWkkBAbSFxC-IfUJhSOSdKJQK-se7uMoJgXUoLuVzJDJTOkIkLG3SzhPPeIYPf5-ad76b9Z9njcn8XZPwfBTng207XWrY5O581Rt6adGPmX0O6fjQ&modules=true
25 |
26 | [presence_example_image]: https://react-pdf-repl.vercel.app/api/og?gz_code=eJy9U01v00AQvedXjNxLKrl2PmiJQhzRFgVxQFRqqUQRh8UeJ4u8Xmd33A8s_3dmYydOCIgb8sHrmeeZeW_eSlVoQ1DBHT6TDzdiiT7cS3zy4Z2OS4U5Rxc6J6ghNVqB99agiOmsSNLQYJ6gQeO96fUcJjC4lJbQ9KseQCqUzF6m4H0qMIdbkVvP57A1McdWRIWdhmHKv9lgaUmQjINYq9CGmvGW4eHj8HWoUE2-iPd2OLr4KB-Kq8uzz4v7hwEGRCnXq0-5d6xzS3AtTAIR9CtYoeC5fOA4oRv9FKI59Ln5zFEDSy8ZRlUFTzKh1RRGg4EPhUgSmS-ncA51PWcso50omxOAkvmNQYt5jJeuflSNz-s2t63XfjJ1bnwrfyKXfuXvom2HK02kFffpMsR97gxTTrXhjFcWBZpYWPQ6TKwzbTh3MplMduG6GWHeflYN8yY4C934e0w63t18wwu_q4wj93iOf9VqV3dVZqETb95ziuPzxjYJpqLMCPqdwFvXHDZbHHuhU9mZroMf7AEsTxlVX8duQ7ymb_WW6maTO3GONgDwo7Qk05frhge3jvnFXvX3MCKTy_wDobJ_ytfb_c53oZkz2V6BRu_IW2GWaTgZenu5VsHIu8cMHnVWFiTs9sA9QdC6RAezuC4FlQbQGG0g19-lDfgWkkBAbSFxC-IfUJhSOSdKJQK-se7uMoJgXUoLuVzJDJTOkIkLG3SzhPPeIYPf5-ad76b9Z9njcn8XZPwfBTng207XWrY5O581Rt6adGPmX0O6fjQ&modules=true
27 |
28 | [dimensions_example]: https://react-pdf-repl.vercel.app/?gz_code=eJyVVl1vmzAUfc-vuGKallQkQCjdlCXZ2lXdHjZtartWa9UHCoZYChgZZ0kX5b_vmo8AaSBBPIDtc--5Pj7XggYR4wLWcEtWQoVftk9UuKNkqcIlcxYBCXH2ioUCNuBxFoDymRPbEf3I9TROQpdwwpWPnY7EDDjxaSwI7647AJ4d0PnLCJSfEQnhxg5jRcXpmDs4NxMiikea5mFYPPBjYQvqDBwWaLHGEB8jXPtrvNcCEnz4Y3-NjeHZD_oQXZz3f1_dPehkIISH-TY95HZYGAvwibhgHAuCCXSfky8VKFa4wgm9B5Mpsp9zbr8MaJy8M1QPPkH6NbBFN4nAqfIcRo9ggQseDYm7ZbxgMnV3DZHtujT0VQhs7tNQhZw-pv8IbCQ3dJF9LJXFN4BLnhd-8hWLlzmZrNfJALapsmGWMBulab-wOeOo4RsylI9SXb1l0T11xWxUCLJVQ-9VsdfUn4latLGDvmBCsKAWPtyBfydefW5zC16mEKnUp8GjoitPUnsjX54RWWOxblTWNxt8adNO4YJrtkzPxJnRuYsO3ZV_Kzj4djQCQ1fBm5PVJeXEEZSFKCxnSyWdvecSoyzxpSDZNCFd56kl-ViTScsVfCN2ZsL9RchOK4qQDXCDe8NKzlQQuHbL0fse4wESL6KIcMeOScJeEI81mSVlJaukhdGc9mIuoFsw5R1cZbt63ZfbrY3lBZBoPVHOz5QiMLPlCMwtFtHpVqfZ4ljLxvkynkX-jSPZLRlyohi6ktKsHy08AUt_2shz3A8e6tAKb75tAZa5ARnMVgxJCAY2BY21kgC5VmlDHyFVCqwoZSKFua-uHFsVSsKNJnhJJ3OIqYcN2D0yGYfKQXnAgr5VaGSpcGod1Ci9JI7QKAUmpWDSQ6e3RQ8TYfDGygVqiFxrJyB7ENtItobLSBy-w47CzsSuIy5eUJzAibap4TLbFFaqy2y1I8NKI_HVGLffknRF3FaOlLavNFClCw54wigHV1rIrNtnYdgdZqva6QccbEkPG1aZPpmydmo4baqhj2h8-s1ppNOtzOm1nsXDSo9N8tXreZRncivvV_YIB-3UcMwdWme_Jl2O2U-WzbDKueqyNHm0TqR2Zq07sXaurb2rjjXtgWY_aNzUtYlxD90bJfO-ujfGmvxHmCa_PvkPRvIj8h_5n2bA&modules=true
29 |
30 | [dimensions_example_image]: https://react-pdf-repl.vercel.app/api/og?gz_code=eJyVVl1vmzAUfc-vuGKallQkQCjdlCXZ2lXdHjZtartWa9UHCoZYChgZZ0kX5b_vmo8AaSBBPIDtc--5Pj7XggYR4wLWcEtWQoVftk9UuKNkqcIlcxYBCXH2ioUCNuBxFoDymRPbEf3I9TROQpdwwpWPnY7EDDjxaSwI7647AJ4d0PnLCJSfEQnhxg5jRcXpmDs4NxMiikea5mFYPPBjYQvqDBwWaLHGEB8jXPtrvNcCEnz4Y3-NjeHZD_oQXZz3f1_dPehkIISH-TY95HZYGAvwibhgHAuCCXSfky8VKFa4wgm9B5Mpsp9zbr8MaJy8M1QPPkH6NbBFN4nAqfIcRo9ggQseDYm7ZbxgMnV3DZHtujT0VQhs7tNQhZw-pv8IbCQ3dJF9LJXFN4BLnhd-8hWLlzmZrNfJALapsmGWMBulab-wOeOo4RsylI9SXb1l0T11xWxUCLJVQ-9VsdfUn4latLGDvmBCsKAWPtyBfydefW5zC16mEKnUp8GjoitPUnsjX54RWWOxblTWNxt8adNO4YJrtkzPxJnRuYsO3ZV_Kzj4djQCQ1fBm5PVJeXEEZSFKCxnSyWdvecSoyzxpSDZNCFd56kl-ViTScsVfCN2ZsL9RchOK4qQDXCDe8NKzlQQuHbL0fse4wESL6KIcMeOScJeEI81mSVlJaukhdGc9mIuoFsw5R1cZbt63ZfbrY3lBZBoPVHOz5QiMLPlCMwtFtHpVqfZ4ljLxvkynkX-jSPZLRlyohi6ktKsHy08AUt_2shz3A8e6tAKb75tAZa5ARnMVgxJCAY2BY21kgC5VmlDHyFVCqwoZSKFua-uHFsVSsKNJnhJJ3OIqYcN2D0yGYfKQXnAgr5VaGSpcGod1Ci9JI7QKAUmpWDSQ6e3RQ8TYfDGygVqiFxrJyB7ENtItobLSBy-w47CzsSuIy5eUJzAibap4TLbFFaqy2y1I8NKI_HVGLffknRF3FaOlLavNFClCw54wigHV1rIrNtnYdgdZqva6QccbEkPG1aZPpmydmo4baqhj2h8-s1ppNOtzOm1nsXDSo9N8tXreZRncivvV_YIB-3UcMwdWme_Jl2O2U-WzbDKueqyNHm0TqR2Zq07sXaurb2rjjXtgWY_aNzUtYlxD90bJfO-ujfGmvxHmCa_PvkPRvIj8h_5n2bA&modules=true
31 |
32 | [watermark_example]: https://react-pdf-repl.vercel.app/?gz_code=eJytVluP4jYUfudXHKVbCdoJiZ0bzADqVtVc1KWtNFvU8tL1EkOiyU1JyMwU8d_72UC4zG6flmBsn_v5fHJMnBZ5WdOmQ_RRvtRXmB-blZr-EHWk5rvdZiXVPIvls5p_yRfrVGZa_kOcPan5Nlf7LS3LPCXjp1KKRW0W4dIqZRbKUpbGTaejhPqlXMVVLcuucrsUaZy8XpPxeyEzehRZZShrVbkALarrorq2rCXUqv6qqkUdL_qLPLUqK4d8BXGrYYGVynTwt7irGPen8bz4-b355-1sbst-XS9hb9uD70WeVTU9C3hORfmkEqQxXBlTn7hPjILISxxiDjkmcyL_ZO0lpkdsGJl-YoLATZcYv_fnUyeAamQ6icnxMBMqenDCLnKEPyR8bf24JkuYqR4emW5iMsh4JhMBBZrPIOFFgXIrfPL3NE5egwhMrzEd4ZG3I8MJaM485VjBFShq8EbZZoLp0HeSHEYc29NDB6KCbxAKTIM3n_qcgsa999gsiBibp-YQYfgNG0KERax1qqKxQXMFAxjHQGydzT7_GZ9PAxegJCo3gAW4FHamDyQdAi1yxYAGh4zJUxYbgBkgQfhHBtpYCxFG4-hoMSI3YftsPZ2bMqpk59OBOgqk4EGa7w7gxBEnu8EpQhH4JAoU4cL7DhCYaRgY3nzKbHZuBvFDbgeW2rvtfncu_oypQPf5cx0abxhi8k452ushnYMvZ3BRPYDgELsqngH5g68Uj5Jxv1A8_Kx4nG9RPHxg6_HF4mGe8wavr8DO_w_2APZw9qjAxMcvymdAzNaoQQFFiehnDGXJPWA-Txkso8IG8OfSkBTewdxo3_ICDesx_lfSmFw27HvOzZ4RovMoon3TkS-694VyKdZJTd0ejSfURUMYHdobVfVrIsebDakGdPu2U9F2O4ECVFSHpAoex8Z739gRQVYNc7-m1lpLIIpkvIpq2GS2_b1ufIdPIcIwzlbXxO1TskjiVfZQy7SC0gIxoq-e8leiOFfZbvfLSUsaqVY_uZdJktNzXiYhAZsatkaW5nSOkqq5U1TK5bjtxau4jtafdRMOY7nK03UprGO3B8CLdVXFQNvizPeMyUl0H-8fHgnfDw-__Xp0YikvLWSWwqyNQSNIy_hFhsfTKPIqruEBEIjPVZ6sa3k8Cq2FS4ye47COxgYfBn3fNfZQjzeH0sBNFSfJ2MjyTJ4FuXlfluK1r-6x7gkZDEpktqqja5ri7uiX-ToLu22lWbq2evQj3rrt1Zli958rinV5xSf0Xj8VRbf7JF_byjt-RndnWyKIjTf42V7Q6xKFuMzLdLz5VOa4IGUXF1RvR0_09p3Sox-Iudvi5YrebS5MaOPgq_gvWFDofTp3ObkQGemLNBxvzm7WA7rf2bZtUF6IRVwjAbvveFuy3ti4O8nDaEP_q8ttGxEYl_LfyKt1d066IPR625MiRUld1uhurV59tR5Zh74x6eD_xn9ik03x&modules=true
33 |
34 | [watermark_example_image]: https://react-pdf-repl.vercel.app/api/og?gz_code=eJytVluP4jYUfudXHKVbCdoJiZ0bzADqVtVc1KWtNFvU8tL1EkOiyU1JyMwU8d_72UC4zG6flmBsn_v5fHJMnBZ5WdOmQ_RRvtRXmB-blZr-EHWk5rvdZiXVPIvls5p_yRfrVGZa_kOcPan5Nlf7LS3LPCXjp1KKRW0W4dIqZRbKUpbGTaejhPqlXMVVLcuucrsUaZy8XpPxeyEzehRZZShrVbkALarrorq2rCXUqv6qqkUdL_qLPLUqK4d8BXGrYYGVynTwt7irGPen8bz4-b355-1sbst-XS9hb9uD70WeVTU9C3hORfmkEqQxXBlTn7hPjILISxxiDjkmcyL_ZO0lpkdsGJl-YoLATZcYv_fnUyeAamQ6icnxMBMqenDCLnKEPyR8bf24JkuYqR4emW5iMsh4JhMBBZrPIOFFgXIrfPL3NE5egwhMrzEd4ZG3I8MJaM485VjBFShq8EbZZoLp0HeSHEYc29NDB6KCbxAKTIM3n_qcgsa999gsiBibp-YQYfgNG0KERax1qqKxQXMFAxjHQGydzT7_GZ9PAxegJCo3gAW4FHamDyQdAi1yxYAGh4zJUxYbgBkgQfhHBtpYCxFG4-hoMSI3YftsPZ2bMqpk59OBOgqk4EGa7w7gxBEnu8EpQhH4JAoU4cL7DhCYaRgY3nzKbHZuBvFDbgeW2rvtfncu_oypQPf5cx0abxhi8k452ushnYMvZ3BRPYDgELsqngH5g68Uj5Jxv1A8_Kx4nG9RPHxg6_HF4mGe8wavr8DO_w_2APZw9qjAxMcvymdAzNaoQQFFiehnDGXJPWA-Txkso8IG8OfSkBTewdxo3_ICDesx_lfSmFw27HvOzZ4RovMoon3TkS-694VyKdZJTd0ejSfURUMYHdobVfVrIsebDakGdPu2U9F2O4ECVFSHpAoex8Z739gRQVYNc7-m1lpLIIpkvIpq2GS2_b1ufIdPIcIwzlbXxO1TskjiVfZQy7SC0gIxoq-e8leiOFfZbvfLSUsaqVY_uZdJktNzXiYhAZsatkaW5nSOkqq5U1TK5bjtxau4jtafdRMOY7nK03UprGO3B8CLdVXFQNvizPeMyUl0H-8fHgnfDw-__Xp0YikvLWSWwqyNQSNIy_hFhsfTKPIqruEBEIjPVZ6sa3k8Cq2FS4ye47COxgYfBn3fNfZQjzeH0sBNFSfJ2MjyTJ4FuXlfluK1r-6x7gkZDEpktqqja5ri7uiX-ToLu22lWbq2evQj3rrt1Zli958rinV5xSf0Xj8VRbf7JF_byjt-RndnWyKIjTf42V7Q6xKFuMzLdLz5VOa4IGUXF1RvR0_09p3Sox-Iudvi5YrebS5MaOPgq_gvWFDofTp3ObkQGemLNBxvzm7WA7rf2bZtUF6IRVwjAbvveFuy3ti4O8nDaEP_q8ttGxEYl_LfyKt1d066IPR625MiRUld1uhurV59tR5Zh74x6eD_xn9ik03x&modules=true
35 |
36 | [tailwind_example]: https://react-pdf-repl.vercel.app/?gz_code=eJyNVNtuG0cMffdXEHqSkL1IRtAUrmTUReC8JE0ApwGaIChGs9w1k7llLlq5gv69nNXFllyg3QdpLuTh4SE5pJ31ETbw2sqk0cQCPogOC_iIa15_IuwLuLUmwhZabzWMfvUoZCxd09YeTYMe_eiXCzrgSL6O-LE_2h_NyyhI9WQaNr_IkJXHjkJEP95cALRCk3q4gtF7hwbuhAmjgo-Dl3x2H6MLV3XdsluouhBFJFlJq-tQW7YPbF6vZq9qjfrnP8WbMLv86R19dr_dlH_cfvo8xSrGlvG2E44trQkRYg-LI9uBQbxHjVeQlwCcPmd32AFIq6wPj3s-SSFazeSk9aZVtke_VAkH1vnb7hbD33YfGteDTA22IqkIbTIykjVw49x4MmB7jMkbGA--80NVIMQHhYvNBrICt8-lgu32eh93ngsIgf7Gxejm5ejgGvvxyJUvoVW4Hn5Kb_vdovfCQSf4djQ5wgBsvlRVdeO9eBjPLidfKy3cePxXATSBxfWe4T5k7pMne4Dv-LDY0Pbk7AmRIewMNJmyL79cTqcufoUDu5K1hmVXZjXL2XTKpE5wMt3FphUq4NOL6xOjeW7gpyEj78vLtRoULJdWNTAc7cp4kvjuu8NdbTYEL2B2SmFeZ_j_EzH8G_Rb61EDuZA0NLmxuFwRhEYeudydHDm3AYiGHAVJpgNUFCv4PSklzsACaocesG1JUvZStERvgQwowYGQ_e6wARIyKQoFaNEZASExsKN4huaUkOhFZB5a28YW4IlNgYGZCkQykprEHamoS0oUICRgosC28C1L-ZwdULMD2fPXR4cVhkjLpFgGhRxKW8UMmeCaNMcMwhGaM0CVZOQ7MhW_TiuR7bRQGJJoBKyScokfBwSeSeGxgncicWxILK_kWeKH4gyvZWXDIxYa0sATyJnxhHqSyGcJWNDACAUsWVzTMOEV3hMLel6NbFjBBy8w5Lnd1dh6SUN2OV-EzosVMVuG5_2PhCC48A1nu0zhDG9FUSCwH4Pei4BKMUmFgwYmq7nvmB-Ja8ZENXGuBbe2ds-wXFIrYlnAsDlW_9HR8zpP9ePJ5DiG8zo_MbubeX14o_Ke37jtxT9q3-Z8&modules=true
37 |
38 | [tailwind_example_image]: https://react-pdf-repl.vercel.app/api/og?gz_code=eJyNVNtuG0cMffdXEHqSkL1IRtAUrmTUReC8JE0ApwGaIChGs9w1k7llLlq5gv69nNXFllyg3QdpLuTh4SE5pJ31ETbw2sqk0cQCPogOC_iIa15_IuwLuLUmwhZabzWMfvUoZCxd09YeTYMe_eiXCzrgSL6O-LE_2h_NyyhI9WQaNr_IkJXHjkJEP95cALRCk3q4gtF7hwbuhAmjgo-Dl3x2H6MLV3XdsluouhBFJFlJq-tQW7YPbF6vZq9qjfrnP8WbMLv86R19dr_dlH_cfvo8xSrGlvG2E44trQkRYg-LI9uBQbxHjVeQlwCcPmd32AFIq6wPj3s-SSFazeSk9aZVtke_VAkH1vnb7hbD33YfGteDTA22IqkIbTIykjVw49x4MmB7jMkbGA--80NVIMQHhYvNBrICt8-lgu32eh93ngsIgf7Gxejm5ejgGvvxyJUvoVW4Hn5Kb_vdovfCQSf4djQ5wgBsvlRVdeO9eBjPLidfKy3cePxXATSBxfWe4T5k7pMne4Dv-LDY0Pbk7AmRIewMNJmyL79cTqcufoUDu5K1hmVXZjXL2XTKpE5wMt3FphUq4NOL6xOjeW7gpyEj78vLtRoULJdWNTAc7cp4kvjuu8NdbTYEL2B2SmFeZ_j_EzH8G_Rb61EDuZA0NLmxuFwRhEYeudydHDm3AYiGHAVJpgNUFCv4PSklzsACaocesG1JUvZStERvgQwowYGQ_e6wARIyKQoFaNEZASExsKN4huaUkOhFZB5a28YW4IlNgYGZCkQykprEHamoS0oUICRgosC28C1L-ZwdULMD2fPXR4cVhkjLpFgGhRxKW8UMmeCaNMcMwhGaM0CVZOQ7MhW_TiuR7bRQGJJoBKyScokfBwSeSeGxgncicWxILK_kWeKH4gyvZWXDIxYa0sATyJnxhHqSyGcJWNDACAUsWVzTMOEV3hMLel6NbFjBBy8w5Lnd1dh6SUN2OV-EzosVMVuG5_2PhCC48A1nu0zhDG9FUSCwH4Pei4BKMUmFgwYmq7nvmB-Ja8ZENXGuBbe2ds-wXFIrYlnAsDlW_9HR8zpP9ePJ5DiG8zo_MbubeX14o_Ke37jtxT9q3-Z8&modules=true
39 |
40 | [image_fallback_example]: https://react-pdf-repl.vercel.app/?gz_code=eJzFVF1v0zAUfe-vsPrUSU3cDhisrBVjrF0RaNM6htjL5CROYohjy75Zu1X571w76ceYeES8xLnX9-Pcc08ipFYGyLpDyFSV0MdzAY8FX-Sce-uTiivJm5srlnF33gq-dOdcto4bvsKAmqRGSdL9YDiLIdBJSg0vE2646b7vdFz90PBMWOCm5zqmTIricUS6l5qXZMFK23XVrInRlwNoO6I0xTQbZhYYiDiMlaSWKoy3GE4fhm-p5PLdDzazw8Ojr-JOfzwNvk1v7wY8BEixXn2AvWNVWiDWTWbJeG_EMEawwD2cSCUIxr0RolmSiDIbkcNB3zsypkdk6I263_HgiyJi8S9PwjZNWQFClYifRVYVFfBukw8K89taBU9haxiR5TsrUgBKbs2nOfK3GpFg2Dbem8Y3_i4gn7ZIcDA_B_LX3wO4MDHebPkUoZBZZTyT8Hqmj09vwp868zCXIoHcveTcoXLtyHhCeug5cUtvGByv121kG0fqeuLhnnhMDsB4jY-a0Enn2UWT3uwhfMZg3WTtgfbZmEpd50nHDc5XXq0JT1lVAOnt0G1kukPodDN9KbAd1quXiJwC2msMcKqeVKWttOvKE3JxPj8jwk_S2wAlwhKbq2V5cEJ9wib7xXrai5bl8fpwMKi3vobIP5yOkd13IBAhk9rh9F9B8049Hor5cesahs7otlU2G9jM8_nqfPaPke4r7Dor-MPFNPIS-wuks8vrBapVFP-B1VWQpEHEbBp4fkEZrB9I5v5RCQMWaKOS0L4KeRXEqC_DimAYMsmeVMmWzSY8JktnRi0hx_8UruPLNY2YKfjjfRTF-f2b41CX-_P7WahTYCPxjXy9zH8DOrGvNw&modules=true
41 |
42 | [image_fallback_example_image]: https://react-pdf-repl.vercel.app/api/og?gz_code=eJzFVF1v0zAUfe-vsPrUSU3cDhisrBVjrF0RaNM6htjL5CROYohjy75Zu1X571w76ceYeES8xLnX9-Pcc08ipFYGyLpDyFSV0MdzAY8FX-Sce-uTiivJm5srlnF33gq-dOdcto4bvsKAmqRGSdL9YDiLIdBJSg0vE2646b7vdFz90PBMWOCm5zqmTIricUS6l5qXZMFK23XVrInRlwNoO6I0xTQbZhYYiDiMlaSWKoy3GE4fhm-p5PLdDzazw8Ojr-JOfzwNvk1v7wY8BEixXn2AvWNVWiDWTWbJeG_EMEawwD2cSCUIxr0RolmSiDIbkcNB3zsypkdk6I263_HgiyJi8S9PwjZNWQFClYifRVYVFfBukw8K89taBU9haxiR5TsrUgBKbs2nOfK3GpFg2Dbem8Y3_i4gn7ZIcDA_B_LX3wO4MDHebPkUoZBZZTyT8Hqmj09vwp868zCXIoHcveTcoXLtyHhCeug5cUtvGByv121kG0fqeuLhnnhMDsB4jY-a0Enn2UWT3uwhfMZg3WTtgfbZmEpd50nHDc5XXq0JT1lVAOnt0G1kukPodDN9KbAd1quXiJwC2msMcKqeVKWttOvKE3JxPj8jwk_S2wAlwhKbq2V5cEJ9wib7xXrai5bl8fpwMKi3vobIP5yOkd13IBAhk9rh9F9B8049Hor5cesahs7otlU2G9jM8_nqfPaPke4r7Dor-MPFNPIS-wuks8vrBapVFP-B1VWQpEHEbBp4fkEZrB9I5v5RCQMWaKOS0L4KeRXEqC_DimAYMsmeVMmWzSY8JktnRi0hx_8UruPLNY2YKfjjfRTF-f2b41CX-_P7WahTYCPxjXy9zH8DOrGvNw&modules=true
43 |
44 | [exif_bug_example]: https://react-pdf-repl.vercel.app/?gz_code=eJzVlV1LwzAUhu_3Kw4BYYOu2dSpzHUoiiB4IQh6KVl62masTUlSujn63z3Z5j4E7-1VkvP5nDcXR-WlNg7W8KhllWPhAngVKQbwrrAO4DnfPN7caoFvGaKDBhKjc2B3BoV0_TJOuMEiRoOG3XY6UhfWgfXxFqKjxFBSgsPuugMw0_FqTD1LEceqSMcwHEATkEP5dt5Tq9hl3j4IQM_mKN2TcmNgVN4JVTAf3vSoHy43_DEmolo46PYgmkKXSk1-BprSg55-qi1XtN7ihR6jAau-MGL3I7YNpFA_-u4OPynrvQEgWeDyURmCUrogKKNrFvzyfxhRkqum48SXevNwcGzKUKWZH46mPTsJ3utzcZIwr6xTyeqBtKD5vCp0kPyHmKbZXad702TzlUdVTrXYKN8cu42MWOZcacecG1GHqXJZNassGrltHEqd09_LypCN41IlfW0UOYTXpY9LkZdUmufCEh1_EUVspSjxcxDOy5Tte_F_CTlsA-R5GyAv2gB52QbIURsgr9oAed0GyJs_ICfcb6jdXuN-sfn7hB82Hm3Gb-Mva9Q&modules=true
45 |
46 | [exif_bug_example_image]: https://react-pdf-repl.vercel.app/api/og?gz_code=eJzVlV1LwzAUhu_3Kw4BYYOu2dSpzHUoiiB4IQh6KVl62masTUlSujn63z3Z5j4E7-1VkvP5nDcXR-WlNg7W8KhllWPhAngVKQbwrrAO4DnfPN7caoFvGaKDBhKjc2B3BoV0_TJOuMEiRoOG3XY6UhfWgfXxFqKjxFBSgsPuugMw0_FqTD1LEceqSMcwHEATkEP5dt5Tq9hl3j4IQM_mKN2TcmNgVN4JVTAf3vSoHy43_DEmolo46PYgmkKXSk1-BprSg55-qi1XtN7ihR6jAau-MGL3I7YNpFA_-u4OPynrvQEgWeDyURmCUrogKKNrFvzyfxhRkqum48SXevNwcGzKUKWZH46mPTsJ3utzcZIwr6xTyeqBtKD5vCp0kPyHmKbZXad702TzlUdVTrXYKN8cu42MWOZcacecG1GHqXJZNassGrltHEqd09_LypCN41IlfW0UOYTXpY9LkZdUmufCEh1_EUVspSjxcxDOy5Tte_F_CTlsA-R5GyAv2gB52QbIURsgr9oAed0GyJs_ICfcb6jdXuN-sfn7hB82Hm3Gb-Mva9Q&modules=true
47 |
--------------------------------------------------------------------------------