@embedpdf/viewer-chrome 3.0.0-next.13 → 3.0.0-next.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -33,6 +33,7 @@ let _embedpdf_react_interaction = require("@embedpdf/react/interaction");
33
33
  let _embedpdf_react_selection = require("@embedpdf/react/selection");
34
34
  let _embedpdf_react_annotation = require("@embedpdf/react/annotation");
35
35
  let _embedpdf_react_stamp = require("@embedpdf/react/stamp");
36
+ let _embedpdf_react_measurement = require("@embedpdf/react/measurement");
36
37
  let _embedpdf_react_redaction = require("@embedpdf/react/redaction");
37
38
  let _embedpdf_react_actions = require("@embedpdf/react/actions");
38
39
  let _embedpdf_react_form = require("@embedpdf/react/form");
@@ -105,6 +106,32 @@ const en = {
105
106
  code: "en",
106
107
  name: "English",
107
108
  translations: {
109
+ measurement: {
110
+ title: "Measurements",
111
+ distance: "Distance",
112
+ perimeter: "Perimeter",
113
+ area: "Area",
114
+ areaUnit: "Area unit",
115
+ resetLabel: "Reset label position",
116
+ calibrate: "Calibrate scale",
117
+ scale: "Scale",
118
+ custom: "Custom",
119
+ unavailable: "Unavailable",
120
+ instructions: "Drag to measure a distance, then click to place its dimension line. Click vertices for a perimeter or area and double-click to finish. Area boundaries must not cross; an unavailable preview cannot be finished. Press Escape to cancel. Drag the label itself to move it.",
121
+ sessionOnly: "This engine keeps page calibration for this session only.",
122
+ allPages: "Apply to all pages",
123
+ recalculate: "Recalculate existing measurements",
124
+ preset: "Scale preset",
125
+ choosePreset: "Choose a preset",
126
+ unit: "Unit",
127
+ precision: "Precision",
128
+ selection: "Selected measurements",
129
+ report: "{updated} updated · {skipped} skipped · {failed} failed",
130
+ knownLength: "Enter the real length represented by the line you just drew.",
131
+ length: "Known length",
132
+ apply: "Apply scale",
133
+ applying: "Applying…"
134
+ },
108
135
  commands: {
109
136
  zoom: {
110
137
  in: "Zoom In",
@@ -148,6 +175,7 @@ const en = {
148
175
  counterclockwise: "Rotate Counter-Clockwise"
149
176
  },
150
177
  mode: {
178
+ measure: "Measure",
151
179
  group: "Mode",
152
180
  view: "View",
153
181
  annotate: "Annotate",
@@ -157,6 +185,7 @@ const en = {
157
185
  redact: "Redact"
158
186
  },
159
187
  annotate: {
188
+ cancelCreation: "Cancel drawing",
160
189
  highlight: "Highlight",
161
190
  strikeout: "Strikeout",
162
191
  underline: "Underline",
@@ -472,7 +501,40 @@ function createViewerHandle(kernel) {
472
501
  *
473
502
  * GENERATED by scratchpad/extract-icons.mjs — 93 icons (slot tags by hand).
474
503
  */
504
+ const measurementOutline = [{
505
+ d: "M5 4H19M20 5V19M19 20H5M4 19V5",
506
+ stroke: "primary"
507
+ }, {
508
+ d: "M3.1 3.1h1.8v1.8H3.1zM19.1 3.1h1.8v1.8h-1.8zM3.1 19.1h1.8v1.8H3.1zM19.1 19.1h1.8v1.8h-1.8z",
509
+ stroke: "primary"
510
+ }];
475
511
  const ICON_PATHS = {
512
+ ruler: ["M3 8h18v8H3z", "M7 8v4M11 8v2M15 8v4M19 8v2"],
513
+ distance: [{
514
+ d: "M5 12H19",
515
+ stroke: "primary",
516
+ strokeDasharray: "2 2"
517
+ }, {
518
+ d: "M3.5 10.5a1.5 1.5 0 1 0 0 3a1.5 1.5 0 1 0 0-3M20.5 10.5a1.5 1.5 0 1 0 0 3a1.5 1.5 0 1 0 0-3",
519
+ stroke: "primary",
520
+ fill: "primary",
521
+ fillOpacity: .25
522
+ }],
523
+ perimeter: measurementOutline,
524
+ area: [
525
+ {
526
+ d: "M5 4H19V5H20V19H19V20H5V19H4V5H5Z",
527
+ fill: "primary",
528
+ fillOpacity: .08
529
+ },
530
+ {
531
+ d: "M4.5 7.5L7.5 4.5M4.5 10.5L10.5 4.5M4.5 13.5L13.5 4.5M4.5 16.5L16.5 4.5M4.5 19.5L19.5 4.5M7.5 19.5L19.5 7.5M10.5 19.5L19.5 10.5M13.5 19.5L19.5 13.5M16.5 19.5L19.5 16.5",
532
+ stroke: "primary",
533
+ strokeWidth: .6,
534
+ strokeOpacity: .5
535
+ },
536
+ ...measurementOutline
537
+ ],
476
538
  alertTriangle: ["M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"],
477
539
  alignBottom: [
478
540
  "M3 19v-14a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z",
@@ -522,6 +584,11 @@ const ICON_PATHS = {
522
584
  "M19 16h-12a2 2 0 0 0 -2 2",
523
585
  "M9 8h6"
524
586
  ],
587
+ calibrate: [
588
+ "M12 12m-7.5 0a7.5 7.5 0 1 0 15 0a7.5 7.5 0 1 0 -15 0",
589
+ "M12 2.75V7M12 17V21.25M2.75 12H7M17 12H21.25",
590
+ "M12 12m-2.5 0a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0 -5 0"
591
+ ],
525
592
  callout: [
526
593
  {
527
594
  d: "M20 6v4a1 1 0 0 1 -1 1h-14a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1h14a1 1 0 0 1 1 1",
@@ -944,6 +1011,11 @@ const ICON_PATHS = {
944
1011
  ],
945
1012
  ungroup: ["M4 5a1 1 0 0 1 1 -1h5a1 1 0 0 1 1 1v5a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1z", "M13 14a1 1 0 0 1 1 -1h5a1 1 0 0 1 1 1v5a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1z"],
946
1013
  unlock: ["M7 11V7a5 5 0 0 1 9.9-1"],
1014
+ updateScale: [
1015
+ "M3.5 13.5L18.5 7.5L21.25 14.5L6.25 20.5L3.5 13.5Z",
1016
+ "M8 12.8L9 15.2M11.5 11.4L12.2 13.2M15 10L16 12.4",
1017
+ "M4 9.39849C4.10585 8.34804 4.487 7.34411 5.10497 6.48809C5.72294 5.63207 6.55584 4.95427 7.51957 4.52312C8.4833 4.09198 9.54373 3.92277 10.5938 4.03257C11.6438 4.14237 12.6463 4.5273 13.5 5.14849M11.5 6.00003L13.5 5.14849L12.7807 2.96999"
1018
+ ],
947
1019
  vertical: [
948
1020
  "M8 7l4 -4l4 4",
949
1021
  "M8 17l4 4l4 -4",
@@ -1013,7 +1085,11 @@ function Icon({ name, size = 20, strokeWidth = 2, className, title, accent }) {
1013
1085
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1014
1086
  d: p.d,
1015
1087
  fill: fill ?? "none",
1016
- stroke
1088
+ stroke,
1089
+ strokeWidth: p.strokeWidth,
1090
+ strokeDasharray: p.strokeDasharray,
1091
+ strokeOpacity: p.strokeOpacity,
1092
+ fillOpacity: p.fillOpacity
1017
1093
  }, i);
1018
1094
  })
1019
1095
  });
@@ -1047,6 +1123,335 @@ function ThemeProvider({ children, preference = "system", target }) {
1047
1123
  });
1048
1124
  }
1049
1125
  //#endregion
1126
+ //#region src/ui/measurement.tsx
1127
+ const control = "border-border bg-surface text-fg w-full rounded-md border px-2 py-1.5 text-sm disabled:opacity-50";
1128
+ const button = "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm disabled:opacity-50";
1129
+ const useCurrentPon = () => (0, _embedpdf_react_runtime.useSelector)(_embedpdf_react_stage.StageToken, (c) => c.pages()[c.currentPage()]?.pon ?? -1);
1130
+ function MeasurementScaleButton() {
1131
+ const t = (0, _embedpdf_react_i18n.useT)();
1132
+ const scale = (0, _embedpdf_react_measurement.usePageScale)(useCurrentPon());
1133
+ const surface = (0, _embedpdf_react_shell.useSurface)("measurement");
1134
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1135
+ className: `${button} inline-flex items-center gap-1.5 whitespace-nowrap`,
1136
+ onClick: () => surface.open({ exclusive: "right" }),
1137
+ title: t("measurement.title"),
1138
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
1139
+ name: "updateScale",
1140
+ size: 20,
1141
+ className: "shrink-0"
1142
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1143
+ t("measurement.scale"),
1144
+ ":",
1145
+ " ",
1146
+ scale.measure?.subtype === "RL" ? scale.measure.ratio ?? t("measurement.custom") : t("measurement.unavailable")
1147
+ ] })]
1148
+ });
1149
+ }
1150
+ function MeasurementSection() {
1151
+ const t = (0, _embedpdf_react_i18n.useT)();
1152
+ const pon = useCurrentPon();
1153
+ const measurement = (0, _embedpdf_react_measurement.useMeasurement)();
1154
+ const scale = (0, _embedpdf_react_measurement.usePageScale)(pon);
1155
+ const anno = (0, _embedpdf_react_runtime.useCapability)(_embedpdf_react_annotation.AnnotationToken);
1156
+ const selected = (0, _embedpdf_react_runtime.useSelector)(_embedpdf_react_annotation.AnnotationToken, (c) => c.selection());
1157
+ const resettable = (0, _embedpdf_react_runtime.useSelector)(_embedpdf_react_annotation.AnnotationToken, (c) => c.getSelected().filter((annotation) => (annotation.subtype === "polygon" || annotation.subtype === "polyline") && annotation.caption?.center && c.canEdit(annotation.ref) && !annotation.flags.lockedContents), (a, b) => a.length === b.length && a.every((annotation, i) => annotation === b[i]));
1158
+ const readouts = (0, _embedpdf_react_runtime.useSelector)(_embedpdf_react_measurement.MeasurementToken, (c) => anno.getSelected().map((d) => c.readout(d.ref)).filter((r) => !("unavailable" in r)), (a, b) => JSON.stringify(a) === JSON.stringify(b));
1159
+ const reports = (0, _embedpdf_react_runtime.useSelector)(_embedpdf_react_measurement.MeasurementToken, (c) => c.lastReports());
1160
+ const [allPages, setAllPages] = (0, react.useState)(false);
1161
+ const [recalculate, setRecalculate] = (0, react.useState)(true);
1162
+ const [error, setError] = (0, react.useState)(null);
1163
+ const disabled = measurement.busy || !measurement.canCalibratePage || !scale.ready;
1164
+ const rectilinear = scale.measure?.subtype === "RL" ? scale.measure : null;
1165
+ const options = {
1166
+ allPages,
1167
+ recalculate
1168
+ };
1169
+ const run = async (work) => {
1170
+ setError(null);
1171
+ try {
1172
+ await work();
1173
+ } catch (e) {
1174
+ setError(e instanceof Error ? e.message : String(e));
1175
+ }
1176
+ };
1177
+ const precision = rectilinear?.distance[rectilinear.distance.length - 1]?.precision ?? 100;
1178
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1179
+ className: "flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4 text-sm",
1180
+ children: [
1181
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1182
+ className: "text-fg-muted",
1183
+ children: t("measurement.instructions")
1184
+ }),
1185
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1186
+ className: "text-fg-muted",
1187
+ children: [t("measurement.scale"), ": "]
1188
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: rectilinear?.ratio ?? t("measurement.unavailable") })] }),
1189
+ scale.error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1190
+ role: "alert",
1191
+ className: "text-red-600",
1192
+ children: scale.error.message
1193
+ }),
1194
+ !scale.persistent && scale.ready && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1195
+ className: "text-fg-muted",
1196
+ children: t("measurement.sessionOnly")
1197
+ }),
1198
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1199
+ className: "flex items-center gap-2",
1200
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1201
+ type: "checkbox",
1202
+ checked: allPages,
1203
+ disabled: measurement.busy,
1204
+ onChange: (e) => setAllPages(e.target.checked)
1205
+ }), t("measurement.allPages")]
1206
+ }),
1207
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1208
+ className: "flex items-center gap-2",
1209
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1210
+ type: "checkbox",
1211
+ checked: recalculate,
1212
+ disabled: measurement.busy,
1213
+ onChange: (e) => setRecalculate(e.target.checked)
1214
+ }), t("measurement.recalculate")]
1215
+ }),
1216
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("measurement.preset"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1217
+ "aria-label": t("measurement.preset"),
1218
+ className: control,
1219
+ value: "",
1220
+ disabled,
1221
+ onChange: (e) => void run(() => measurement.setPreset(pon, e.target.value, options)),
1222
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1223
+ value: "",
1224
+ disabled: true,
1225
+ children: t("measurement.choosePreset")
1226
+ }), measurement.presets().map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1227
+ value: p.id,
1228
+ children: p.label
1229
+ }, p.id))]
1230
+ })] }),
1231
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("measurement.unit"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1232
+ "aria-label": t("measurement.unit"),
1233
+ className: control,
1234
+ value: rectilinear?.distance[0]?.unit.trim() ?? "",
1235
+ disabled: disabled || !rectilinear,
1236
+ onChange: (e) => void run(() => measurement.setUnit(pon, e.target.value, void 0, options)),
1237
+ children: [!measurement.units().includes(rectilinear?.distance[0]?.unit.trim()) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1238
+ value: rectilinear?.distance[0]?.unit.trim() ?? "",
1239
+ children: t("measurement.custom")
1240
+ }), measurement.units().map((u) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1241
+ value: u,
1242
+ children: u
1243
+ }, u))]
1244
+ })] }),
1245
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("measurement.areaUnit"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1246
+ "aria-label": t("measurement.areaUnit"),
1247
+ className: control,
1248
+ value: rectilinear?.area[0]?.unit.trim().replace("²", "2") ?? "",
1249
+ disabled: disabled || !rectilinear,
1250
+ onChange: (e) => void run(() => measurement.setAreaUnit(pon, e.target.value, options)),
1251
+ children: [!measurement.areaUnits().includes(rectilinear?.area[0]?.unit.trim().replace("²", "2")) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1252
+ value: rectilinear?.area[0]?.unit.trim().replace("²", "2") ?? "",
1253
+ children: t("measurement.custom")
1254
+ }), measurement.areaUnits().map((unit) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1255
+ value: unit,
1256
+ children: unit.replace("2", "²")
1257
+ }, unit))]
1258
+ })] }),
1259
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("measurement.precision"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1260
+ "aria-label": t("measurement.precision"),
1261
+ className: control,
1262
+ value: precision,
1263
+ disabled: disabled || !rectilinear,
1264
+ onChange: (e) => void run(() => measurement.setPrecision(pon, Number(e.target.value), options)),
1265
+ children: [![
1266
+ 1,
1267
+ 10,
1268
+ 100,
1269
+ 1e3,
1270
+ 1e4
1271
+ ].includes(precision) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1272
+ value: precision,
1273
+ children: t("measurement.custom")
1274
+ }), [
1275
+ 1,
1276
+ 10,
1277
+ 100,
1278
+ 1e3,
1279
+ 1e4
1280
+ ].map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1281
+ value: p,
1282
+ children: 1 / p
1283
+ }, p))]
1284
+ })] }),
1285
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1286
+ className: button,
1287
+ disabled,
1288
+ onClick: () => measurement.startCalibration(),
1289
+ children: t("measurement.calibrate")
1290
+ }),
1291
+ selected.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1292
+ className: "border-border-subtle border-t pt-3",
1293
+ children: [
1294
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1295
+ className: "mb-1 font-medium",
1296
+ children: t("measurement.selection")
1297
+ }),
1298
+ readouts.length ? readouts.map((readout, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1299
+ className: "mb-2",
1300
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
1301
+ t(`measurement.${readout.kind}`),
1302
+ ": ",
1303
+ readout.label
1304
+ ] }), readout.perimeter && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
1305
+ t("measurement.perimeter"),
1306
+ ": ",
1307
+ readout.perimeter
1308
+ ] })]
1309
+ }, i)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1310
+ className: "text-fg-muted",
1311
+ children: t("measurement.unavailable")
1312
+ }),
1313
+ resettable.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1314
+ className: button,
1315
+ disabled: measurement.busy,
1316
+ onClick: () => void run(async () => {
1317
+ for (const annotation of resettable) if (annotation.subtype === "polygon" || annotation.subtype === "polyline") await anno.update(annotation.ref, {
1318
+ subtype: annotation.subtype,
1319
+ caption: { center: null }
1320
+ });
1321
+ }),
1322
+ children: t("measurement.resetLabel")
1323
+ })
1324
+ ]
1325
+ }),
1326
+ reports.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1327
+ role: "status",
1328
+ children: t("measurement.report", { params: {
1329
+ updated: reports.reduce((n, r) => n + r.updated.length, 0),
1330
+ skipped: reports.reduce((n, r) => n + r.skipped.length, 0),
1331
+ failed: reports.reduce((n, r) => n + r.failed.length + (r.scaleError || r.error ? 1 : 0), 0)
1332
+ } })
1333
+ }),
1334
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1335
+ role: "alert",
1336
+ className: "text-red-600",
1337
+ children: error
1338
+ })
1339
+ ]
1340
+ });
1341
+ }
1342
+ function CalibrationDialog() {
1343
+ const t = (0, _embedpdf_react_i18n.useT)();
1344
+ const measurement = (0, _embedpdf_react_measurement.useMeasurement)();
1345
+ const request = (0, _embedpdf_react_runtime.useSelector)(_embedpdf_react_measurement.MeasurementToken, (c) => c.calibrationRequest());
1346
+ const input = (0, react.useRef)(null);
1347
+ const [value, setValue] = (0, react.useState)("");
1348
+ const [unit, setUnit] = (0, react.useState)("m");
1349
+ const [allPages, setAllPages] = (0, react.useState)(false);
1350
+ const [recalculate, setRecalculate] = (0, react.useState)(true);
1351
+ const [error, setError] = (0, react.useState)(null);
1352
+ (0, react.useEffect)(() => {
1353
+ setValue("");
1354
+ setError(null);
1355
+ input.current?.focus();
1356
+ }, [request]);
1357
+ if (!request) return null;
1358
+ const submit = async () => {
1359
+ setError(null);
1360
+ try {
1361
+ await measurement.calibrate(request.pon, request.from, request.to, {
1362
+ value: Number(value),
1363
+ unit
1364
+ }, {
1365
+ allPages,
1366
+ recalculate
1367
+ });
1368
+ measurement.dismissCalibration();
1369
+ } catch (e) {
1370
+ setError(e instanceof Error ? e.message : String(e));
1371
+ }
1372
+ };
1373
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1374
+ className: "fixed inset-0 z-50 grid place-items-center bg-black/40",
1375
+ onKeyDown: (e) => {
1376
+ if (e.key === "Escape" && !measurement.busy) measurement.dismissCalibration();
1377
+ },
1378
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1379
+ role: "dialog",
1380
+ "aria-modal": "true",
1381
+ "aria-labelledby": "measurement-calibration-title",
1382
+ className: "border-border bg-surface flex w-96 flex-col gap-4 rounded-lg border p-5 shadow-xl",
1383
+ onSubmit: (e) => {
1384
+ e.preventDefault();
1385
+ submit();
1386
+ },
1387
+ children: [
1388
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1389
+ id: "measurement-calibration-title",
1390
+ className: "font-semibold",
1391
+ children: t("measurement.calibrate")
1392
+ }),
1393
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1394
+ className: "text-fg-muted text-sm",
1395
+ children: t("measurement.knownLength")
1396
+ }),
1397
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("measurement.length"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1398
+ ref: input,
1399
+ "aria-label": t("measurement.length"),
1400
+ className: control,
1401
+ type: "number",
1402
+ min: "0",
1403
+ step: "any",
1404
+ value,
1405
+ onChange: (e) => setValue(e.target.value)
1406
+ })] }),
1407
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("measurement.unit"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1408
+ className: control,
1409
+ "aria-label": t("measurement.unit"),
1410
+ value: unit,
1411
+ onChange: (e) => setUnit(e.target.value),
1412
+ children: measurement.units().map((u) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", { children: u }, u))
1413
+ })] }),
1414
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1415
+ className: "flex items-center gap-2 text-sm",
1416
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1417
+ type: "checkbox",
1418
+ checked: allPages,
1419
+ onChange: (e) => setAllPages(e.target.checked)
1420
+ }), t("measurement.allPages")]
1421
+ }),
1422
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1423
+ className: "flex items-center gap-2 text-sm",
1424
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1425
+ type: "checkbox",
1426
+ checked: recalculate,
1427
+ onChange: (e) => setRecalculate(e.target.checked)
1428
+ }), t("measurement.recalculate")]
1429
+ }),
1430
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1431
+ role: "alert",
1432
+ className: "text-sm text-red-600",
1433
+ children: error
1434
+ }),
1435
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1436
+ className: "flex justify-end gap-2",
1437
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1438
+ type: "button",
1439
+ className: button,
1440
+ disabled: measurement.busy,
1441
+ onClick: () => measurement.dismissCalibration(),
1442
+ children: t("demo.cancel")
1443
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1444
+ className: button,
1445
+ type: "submit",
1446
+ disabled: measurement.busy || !measurement.canCalibratePage || !(Number(value) > 0) || !Number.isFinite(Number(value)),
1447
+ children: measurement.busy ? t("measurement.applying") : t("measurement.apply")
1448
+ })]
1449
+ })
1450
+ ]
1451
+ })
1452
+ });
1453
+ }
1454
+ //#endregion
1050
1455
  //#region src/config/default-stamps.ts
1051
1456
  /**
1052
1457
  * The viewer's BUILT-IN stamp library: `@embedpdf/default-stamps`, the
@@ -1616,6 +2021,7 @@ function AppToolbar({ bar, className }) {
1616
2021
  run
1617
2022
  }),
1618
2023
  renderCustom: {
2024
+ "measurement-scale": () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MeasurementScaleButton, {}),
1619
2025
  "zoom-controls": (variant) => variant === "inline" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomControls, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomButton, {}),
1620
2026
  "quick-stamps": () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickStamps, {})
1621
2027
  },
@@ -2062,7 +2468,13 @@ function iconGlyph(name, accent) {
2062
2468
  const fill = spec.fill === true ? INK : spec.fill ? accent?.[spec.fill] : void 0;
2063
2469
  const stroke = spec.stroke ? accent?.[spec.stroke] ?? INK : spec.fill ? "none" : INK;
2064
2470
  halo.push(`<path d="${spec.d}" fill="none" stroke="${HALO}" stroke-width="3.5"/>`);
2065
- draw.push(`<path d="${spec.d}" fill="${fill ?? "none"}" stroke="${stroke}"/>`);
2471
+ const decoration = [
2472
+ ["stroke-width", spec.strokeWidth],
2473
+ ["stroke-dasharray", spec.strokeDasharray],
2474
+ ["stroke-opacity", spec.strokeOpacity],
2475
+ ["fill-opacity", spec.fillOpacity]
2476
+ ].filter(([, value]) => value !== void 0).map(([key, value]) => `${key}="${value}"`).join(" ");
2477
+ draw.push(`<path d="${spec.d}" fill="${fill ?? "none"}" stroke="${stroke}" ${decoration}/>`);
2066
2478
  }
2067
2479
  return halo.join("") + draw.join("");
2068
2480
  }
@@ -4471,12 +4883,13 @@ function RightSidebar() {
4471
4883
  const comment = (0, _embedpdf_react_shell.useSurface)("comment");
4472
4884
  const style = (0, _embedpdf_react_shell.useSurface)("annotation-style");
4473
4885
  const redaction = (0, _embedpdf_react_shell.useSurface)("redaction");
4886
+ const measurement = (0, _embedpdf_react_shell.useSurface)("measurement");
4474
4887
  const stamps = (0, _embedpdf_react_shell.useSurface)("stamps");
4475
4888
  const signatures = (0, _embedpdf_react_shell.useSurface)("signatures");
4476
- const active = search.isOpen ? "search" : comment.isOpen ? "comment" : style.isOpen ? "style" : redaction.isOpen ? "redaction" : stamps.isOpen ? "stamps" : signatures.isOpen ? "signatures" : null;
4889
+ const active = measurement.isOpen ? "measurement" : search.isOpen ? "search" : comment.isOpen ? "comment" : style.isOpen ? "style" : redaction.isOpen ? "redaction" : stamps.isOpen ? "stamps" : signatures.isOpen ? "signatures" : null;
4477
4890
  if (!active) return null;
4478
- const titleKey = active === "search" ? "demo.searchTitle" : active === "comment" ? "demo.commentsTitle" : active === "redaction" ? "demo.redactionTitle" : active === "stamps" ? "demo.stampsTitle" : active === "signatures" ? "demo.signaturesTitle" : "demo.styleTitle";
4479
- const close = active === "search" ? search.close : active === "comment" ? comment.close : active === "redaction" ? redaction.close : active === "stamps" ? stamps.close : active === "signatures" ? signatures.close : style.close;
4891
+ const titleKey = active === "measurement" ? "measurement.title" : active === "search" ? "demo.searchTitle" : active === "comment" ? "demo.commentsTitle" : active === "redaction" ? "demo.redactionTitle" : active === "stamps" ? "demo.stampsTitle" : active === "signatures" ? "demo.signaturesTitle" : "demo.styleTitle";
4892
+ const close = active === "measurement" ? measurement.close : active === "search" ? search.close : active === "comment" ? comment.close : active === "redaction" ? redaction.close : active === "stamps" ? stamps.close : active === "signatures" ? signatures.close : style.close;
4480
4893
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
4481
4894
  className: "border-border-subtle bg-surface flex w-72 shrink-0 flex-col border-l",
4482
4895
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -4493,7 +4906,7 @@ function RightSidebar() {
4493
4906
  size: 16
4494
4907
  })
4495
4908
  })]
4496
- }), active === "style" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationStylePanel, {}) : active === "search" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchPanel, {}, documentId ?? "none") : active === "redaction" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RedactionPanel, {}) : active === "stamps" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StampsPanel, {}) : active === "signatures" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignaturesPanel, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommentsPanel, {}, documentId ?? "none")]
4909
+ }), active === "measurement" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MeasurementSection, {}, documentId ?? "none") : active === "style" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationStylePanel, {}) : active === "search" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchPanel, {}, documentId ?? "none") : active === "redaction" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RedactionPanel, {}) : active === "stamps" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StampsPanel, {}) : active === "signatures" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignaturesPanel, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommentsPanel, {}, documentId ?? "none")]
4497
4910
  });
4498
4911
  }
4499
4912
  function PageControls() {
@@ -5546,6 +5959,7 @@ function Shell() {
5546
5959
  }),
5547
5960
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RightSidebar, {}),
5548
5961
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RedactConfirmModal, {}),
5962
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CalibrationDialog, {}),
5549
5963
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignatureBridge, {}),
5550
5964
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignDialog, {}),
5551
5965
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignatureMakerModal, {})
@@ -5576,7 +5990,7 @@ function Shell() {
5576
5990
  * changing these props on a mounted viewer does not rebuild the workspace.
5577
5991
  */
5578
5992
  /** Lazy built-in packs — a strings override for one of these wraps its loader. */
5579
- const BUILTIN_LOADERS = { es: () => Promise.resolve().then(() => require("./es-DYwzfxyt.cjs")).then((m) => m.es) };
5993
+ const BUILTIN_LOADERS = { es: () => Promise.resolve().then(() => require("./es-BXLlBo2b.cjs")).then((m) => m.es) };
5580
5994
  const isDict = (v) => typeof v === "object" && v !== null;
5581
5995
  /**
5582
5996
  * Deep-merge `over` into `base` (immutably), expanding dotted keys —
@@ -5727,6 +6141,7 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
5727
6141
  }),
5728
6142
  (0, _embedpdf_react_link.linkPlugin)(),
5729
6143
  (0, _embedpdf_react_redaction.redactionPlugin)(),
6144
+ (0, _embedpdf_react_measurement.measurementPlugin)(),
5730
6145
  (0, _embedpdf_react_search.searchPlugin)(),
5731
6146
  demoToolsPlugin(),
5732
6147
  (0, _embedpdf_react_i18n.i18nPlugin)({
@@ -5795,6 +6210,12 @@ Object.defineProperty(exports, "InteractionToken", {
5795
6210
  return _embedpdf_react_interaction.InteractionToken;
5796
6211
  }
5797
6212
  });
6213
+ Object.defineProperty(exports, "MeasurementToken", {
6214
+ enumerable: true,
6215
+ get: function() {
6216
+ return _embedpdf_react_measurement.MeasurementToken;
6217
+ }
6218
+ });
5798
6219
  Object.defineProperty(exports, "MetadataToken", {
5799
6220
  enumerable: true,
5800
6221
  get: function() {