react-cheminfo 0.76.0 → 0.77.0

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.
Files changed (316) hide show
  1. package/README.md +141 -31
  2. package/lib/card/core/cardAddress.d.ts +19 -2
  3. package/lib/card/core/cardAddress.d.ts.map +1 -1
  4. package/lib/card/core/cardAddress.js +35 -2
  5. package/lib/card/core/cardAddress.js.map +1 -1
  6. package/lib/card/core/cardPageUrl.d.ts +7 -1
  7. package/lib/card/core/cardPageUrl.d.ts.map +1 -1
  8. package/lib/card/core/cardPageUrl.js +4 -2
  9. package/lib/card/core/cardPageUrl.js.map +1 -1
  10. package/lib/card/core/index.d.ts +1 -1
  11. package/lib/card/core/index.d.ts.map +1 -1
  12. package/lib/card/core/index.js +1 -1
  13. package/lib/card/core/index.js.map +1 -1
  14. package/lib/credits/core/credits.d.ts +30 -0
  15. package/lib/credits/core/credits.d.ts.map +1 -1
  16. package/lib/credits/core/credits.js +35 -0
  17. package/lib/credits/core/credits.js.map +1 -1
  18. package/lib/download/core/downloadFigure.d.ts +12 -1
  19. package/lib/download/core/downloadFigure.d.ts.map +1 -1
  20. package/lib/download/core/downloadFigure.js +21 -0
  21. package/lib/download/core/downloadFigure.js.map +1 -1
  22. package/lib/download/core/figureDpi.d.ts +59 -0
  23. package/lib/download/core/figureDpi.d.ts.map +1 -0
  24. package/lib/download/core/figureDpi.js +75 -0
  25. package/lib/download/core/figureDpi.js.map +1 -0
  26. package/lib/download/core/figurePng.d.ts +4 -1
  27. package/lib/download/core/figurePng.d.ts.map +1 -1
  28. package/lib/download/core/figurePng.js +11 -2
  29. package/lib/download/core/figurePng.js.map +1 -1
  30. package/lib/download/core/index.d.ts +4 -1
  31. package/lib/download/core/index.d.ts.map +1 -1
  32. package/lib/download/core/index.js +3 -1
  33. package/lib/download/core/index.js.map +1 -1
  34. package/lib/download/core/pngDpi.d.ts +22 -0
  35. package/lib/download/core/pngDpi.d.ts.map +1 -0
  36. package/lib/download/core/pngDpi.js +79 -0
  37. package/lib/download/core/pngDpi.js.map +1 -0
  38. package/lib/download/ui/FigureDownloadPanel.d.ts +13 -0
  39. package/lib/download/ui/FigureDownloadPanel.d.ts.map +1 -1
  40. package/lib/download/ui/FigureDownloadPanel.js +18 -34
  41. package/lib/download/ui/FigureDownloadPanel.js.map +1 -1
  42. package/lib/download/ui/figureDownloadHint.d.ts +5 -0
  43. package/lib/download/ui/figureDownloadHint.d.ts.map +1 -1
  44. package/lib/download/ui/figureDownloadHint.js +8 -0
  45. package/lib/download/ui/figureDownloadHint.js.map +1 -1
  46. package/lib/download/ui/figureDownloadOptions.d.ts +84 -0
  47. package/lib/download/ui/figureDownloadOptions.d.ts.map +1 -0
  48. package/lib/download/ui/figureDownloadOptions.js +2 -0
  49. package/lib/download/ui/figureDownloadOptions.js.map +1 -0
  50. package/lib/download/ui/resolutionChoices.d.ts +27 -0
  51. package/lib/download/ui/resolutionChoices.d.ts.map +1 -0
  52. package/lib/download/ui/resolutionChoices.js +48 -0
  53. package/lib/download/ui/resolutionChoices.js.map +1 -0
  54. package/lib/download/ui/useFigureActions.d.ts +4 -1
  55. package/lib/download/ui/useFigureActions.d.ts.map +1 -1
  56. package/lib/download/ui/useFigureActions.js +17 -8
  57. package/lib/download/ui/useFigureActions.js.map +1 -1
  58. package/lib/download/ui/useFigureDownload.d.ts +2 -55
  59. package/lib/download/ui/useFigureDownload.d.ts.map +1 -1
  60. package/lib/download/ui/useFigureDownload.js +45 -13
  61. package/lib/download/ui/useFigureDownload.js.map +1 -1
  62. package/lib/ecosystem/core/sites.d.ts.map +1 -1
  63. package/lib/ecosystem/core/sites.js +14 -0
  64. package/lib/ecosystem/core/sites.js.map +1 -1
  65. package/lib/ecosystem/core/types.d.ts +1 -1
  66. package/lib/ecosystem/core/types.d.ts.map +1 -1
  67. package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
  68. package/lib/ecosystem/ui/glyphs.js +3 -0
  69. package/lib/ecosystem/ui/glyphs.js.map +1 -1
  70. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  71. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  72. package/lib/locales/de.json +30 -1
  73. package/lib/locales/en.json +30 -1
  74. package/lib/locales/es.json +30 -1
  75. package/lib/locales/fr.json +30 -1
  76. package/lib/locales/it.json +30 -1
  77. package/lib/molecule3d/ui/Molecule3DStage.d.ts +4 -33
  78. package/lib/molecule3d/ui/Molecule3DStage.d.ts.map +1 -1
  79. package/lib/molecule3d/ui/Molecule3DStage.js +3 -37
  80. package/lib/molecule3d/ui/Molecule3DStage.js.map +1 -1
  81. package/lib/osiris/core/barGeometry.d.ts +42 -0
  82. package/lib/osiris/core/barGeometry.d.ts.map +1 -0
  83. package/lib/osiris/core/barGeometry.js +46 -0
  84. package/lib/osiris/core/barGeometry.js.map +1 -0
  85. package/lib/osiris/core/computeProperties.d.ts +42 -0
  86. package/lib/osiris/core/computeProperties.d.ts.map +1 -0
  87. package/lib/osiris/core/computeProperties.js +115 -0
  88. package/lib/osiris/core/computeProperties.js.map +1 -0
  89. package/lib/osiris/core/constants.d.ts +70 -0
  90. package/lib/osiris/core/constants.d.ts.map +1 -0
  91. package/lib/osiris/core/constants.js +99 -0
  92. package/lib/osiris/core/constants.js.map +1 -0
  93. package/lib/osiris/core/fitness.d.ts +32 -0
  94. package/lib/osiris/core/fitness.d.ts.map +1 -0
  95. package/lib/osiris/core/fitness.js +48 -0
  96. package/lib/osiris/core/fitness.js.map +1 -0
  97. package/lib/osiris/core/index.d.ts +29 -0
  98. package/lib/osiris/core/index.d.ts.map +1 -0
  99. package/lib/osiris/core/index.js +19 -0
  100. package/lib/osiris/core/index.js.map +1 -0
  101. package/lib/osiris/core/poolSize.d.ts +17 -0
  102. package/lib/osiris/core/poolSize.d.ts.map +1 -0
  103. package/lib/osiris/core/poolSize.js +22 -0
  104. package/lib/osiris/core/poolSize.js.map +1 -0
  105. package/lib/osiris/core/poolTypes.d.ts +140 -0
  106. package/lib/osiris/core/poolTypes.d.ts.map +1 -0
  107. package/lib/osiris/core/poolTypes.js +12 -0
  108. package/lib/osiris/core/poolTypes.js.map +1 -0
  109. package/lib/osiris/core/predictIdCode.d.ts +39 -0
  110. package/lib/osiris/core/predictIdCode.d.ts.map +1 -0
  111. package/lib/osiris/core/predictIdCode.js +54 -0
  112. package/lib/osiris/core/predictIdCode.js.map +1 -0
  113. package/lib/osiris/core/predictProtocol.d.ts +67 -0
  114. package/lib/osiris/core/predictProtocol.d.ts.map +1 -0
  115. package/lib/osiris/core/predictProtocol.js +29 -0
  116. package/lib/osiris/core/predictProtocol.js.map +1 -0
  117. package/lib/osiris/core/predictionPool.d.ts +23 -0
  118. package/lib/osiris/core/predictionPool.d.ts.map +1 -0
  119. package/lib/osiris/core/predictionPool.js +186 -0
  120. package/lib/osiris/core/predictionPool.js.map +1 -0
  121. package/lib/osiris/core/predictionQueue.d.ts +46 -0
  122. package/lib/osiris/core/predictionQueue.d.ts.map +1 -0
  123. package/lib/osiris/core/predictionQueue.js +57 -0
  124. package/lib/osiris/core/predictionQueue.js.map +1 -0
  125. package/lib/osiris/core/predictors.d.ts +62 -0
  126. package/lib/osiris/core/predictors.d.ts.map +1 -0
  127. package/lib/osiris/core/predictors.js +100 -0
  128. package/lib/osiris/core/predictors.js.map +1 -0
  129. package/lib/osiris/core/propertyKeys.d.ts +32 -0
  130. package/lib/osiris/core/propertyKeys.d.ts.map +1 -0
  131. package/lib/osiris/core/propertyKeys.js +44 -0
  132. package/lib/osiris/core/propertyKeys.js.map +1 -0
  133. package/lib/osiris/core/riskAppearance.d.ts +60 -0
  134. package/lib/osiris/core/riskAppearance.d.ts.map +1 -0
  135. package/lib/osiris/core/riskAppearance.js +59 -0
  136. package/lib/osiris/core/riskAppearance.js.map +1 -0
  137. package/lib/osiris/core/riskDetail.d.ts +22 -0
  138. package/lib/osiris/core/riskDetail.d.ts.map +1 -0
  139. package/lib/osiris/core/riskDetail.js +86 -0
  140. package/lib/osiris/core/riskDetail.js.map +1 -0
  141. package/lib/osiris/core/riskDetailRequest.d.ts +31 -0
  142. package/lib/osiris/core/riskDetailRequest.d.ts.map +1 -0
  143. package/lib/osiris/core/riskDetailRequest.js +49 -0
  144. package/lib/osiris/core/riskDetailRequest.js.map +1 -0
  145. package/lib/osiris/core/riskFindings.d.ts +26 -0
  146. package/lib/osiris/core/riskFindings.d.ts.map +1 -0
  147. package/lib/osiris/core/riskFindings.js +79 -0
  148. package/lib/osiris/core/riskFindings.js.map +1 -0
  149. package/lib/osiris/core/runPredictionJob.d.ts +20 -0
  150. package/lib/osiris/core/runPredictionJob.d.ts.map +1 -0
  151. package/lib/osiris/core/runPredictionJob.js +43 -0
  152. package/lib/osiris/core/runPredictionJob.js.map +1 -0
  153. package/lib/osiris/core/scales.d.ts +70 -0
  154. package/lib/osiris/core/scales.d.ts.map +1 -0
  155. package/lib/osiris/core/scales.js +185 -0
  156. package/lib/osiris/core/scales.js.map +1 -0
  157. package/lib/osiris/core/types.d.ts +91 -0
  158. package/lib/osiris/core/types.d.ts.map +1 -0
  159. package/lib/osiris/core/types.js +19 -0
  160. package/lib/osiris/core/types.js.map +1 -0
  161. package/lib/osiris/core/valuation.d.ts +28 -0
  162. package/lib/osiris/core/valuation.d.ts.map +1 -0
  163. package/lib/osiris/core/valuation.js +39 -0
  164. package/lib/osiris/core/valuation.js.map +1 -0
  165. package/lib/osiris/ui/PropertyBar.d.ts +27 -0
  166. package/lib/osiris/ui/PropertyBar.d.ts.map +1 -0
  167. package/lib/osiris/ui/PropertyBar.js +27 -0
  168. package/lib/osiris/ui/PropertyBar.js.map +1 -0
  169. package/lib/osiris/ui/PropertyPanel.d.ts +91 -0
  170. package/lib/osiris/ui/PropertyPanel.d.ts.map +1 -0
  171. package/lib/osiris/ui/PropertyPanel.js +77 -0
  172. package/lib/osiris/ui/PropertyPanel.js.map +1 -0
  173. package/lib/osiris/ui/PropertyRows.d.ts +40 -0
  174. package/lib/osiris/ui/PropertyRows.d.ts.map +1 -0
  175. package/lib/osiris/ui/PropertyRows.js +23 -0
  176. package/lib/osiris/ui/PropertyRows.js.map +1 -0
  177. package/lib/osiris/ui/RiskFindings.d.ts +43 -0
  178. package/lib/osiris/ui/RiskFindings.d.ts.map +1 -0
  179. package/lib/osiris/ui/RiskFindings.js +51 -0
  180. package/lib/osiris/ui/RiskFindings.js.map +1 -0
  181. package/lib/osiris/ui/RiskPanel.d.ts +46 -0
  182. package/lib/osiris/ui/RiskPanel.d.ts.map +1 -0
  183. package/lib/osiris/ui/RiskPanel.js +32 -0
  184. package/lib/osiris/ui/RiskPanel.js.map +1 -0
  185. package/lib/osiris/ui/RiskSquare.d.ts +51 -0
  186. package/lib/osiris/ui/RiskSquare.d.ts.map +1 -0
  187. package/lib/osiris/ui/RiskSquare.js +23 -0
  188. package/lib/osiris/ui/RiskSquare.js.map +1 -0
  189. package/lib/osiris/ui/index.d.ts +15 -0
  190. package/lib/osiris/ui/index.d.ts.map +1 -0
  191. package/lib/osiris/ui/index.js +8 -0
  192. package/lib/osiris/ui/index.js.map +1 -0
  193. package/lib/osiris/ui/usePredictions.d.ts +81 -0
  194. package/lib/osiris/ui/usePredictions.d.ts.map +1 -0
  195. package/lib/osiris/ui/usePredictions.js +102 -0
  196. package/lib/osiris/ui/usePredictions.js.map +1 -0
  197. package/lib/osiris-ui.d.ts +11 -0
  198. package/lib/osiris-ui.d.ts.map +1 -0
  199. package/lib/osiris-ui.js +11 -0
  200. package/lib/osiris-ui.js.map +1 -0
  201. package/lib/osiris.d.ts +17 -0
  202. package/lib/osiris.d.ts.map +1 -0
  203. package/lib/osiris.js +17 -0
  204. package/lib/osiris.js.map +1 -0
  205. package/lib/panel/ui/FilterInput.d.ts.map +1 -1
  206. package/lib/panel/ui/FilterInput.js +3 -1
  207. package/lib/panel/ui/FilterInput.js.map +1 -1
  208. package/lib/panel/ui/SidePanelItem.d.ts +39 -0
  209. package/lib/panel/ui/SidePanelItem.d.ts.map +1 -0
  210. package/lib/panel/ui/SidePanelItem.js +63 -0
  211. package/lib/panel/ui/SidePanelItem.js.map +1 -0
  212. package/lib/panel/ui/SidePanelStack.d.ts +11 -10
  213. package/lib/panel/ui/SidePanelStack.d.ts.map +1 -1
  214. package/lib/panel/ui/SidePanelStack.js +22 -15
  215. package/lib/panel/ui/SidePanelStack.js.map +1 -1
  216. package/lib/panel/ui/SpectrumColorSwatch.d.ts.map +1 -1
  217. package/lib/panel/ui/SpectrumColorSwatch.js +4 -2
  218. package/lib/panel/ui/SpectrumColorSwatch.js.map +1 -1
  219. package/lib/panel/ui/usePanelFolds.d.ts +35 -0
  220. package/lib/panel/ui/usePanelFolds.d.ts.map +1 -0
  221. package/lib/panel/ui/usePanelFolds.js +73 -0
  222. package/lib/panel/ui/usePanelFolds.js.map +1 -0
  223. package/lib/shared/ui/FullscreenStage.d.ts +43 -0
  224. package/lib/shared/ui/FullscreenStage.d.ts.map +1 -0
  225. package/lib/shared/ui/FullscreenStage.js +38 -0
  226. package/lib/shared/ui/FullscreenStage.js.map +1 -0
  227. package/lib/shared/ui/canFillScreen.d.ts +7 -0
  228. package/lib/shared/ui/canFillScreen.d.ts.map +1 -0
  229. package/lib/shared/ui/canFillScreen.js +9 -0
  230. package/lib/shared/ui/canFillScreen.js.map +1 -0
  231. package/lib/shared/ui/index.d.ts +3 -0
  232. package/lib/shared/ui/index.d.ts.map +1 -1
  233. package/lib/shared/ui/index.js +2 -0
  234. package/lib/shared/ui/index.js.map +1 -1
  235. package/lib/split/ui/splitPanes.d.ts +3 -3
  236. package/lib/split/ui/splitPanes.js +1 -1
  237. package/lib/structure/core/structureNotations.d.ts +8 -1
  238. package/lib/structure/core/structureNotations.d.ts.map +1 -1
  239. package/lib/structure/core/structureNotations.js +14 -8
  240. package/lib/structure/core/structureNotations.js.map +1 -1
  241. package/lib/structure/ui/StructureExportPanel.d.ts +8 -0
  242. package/lib/structure/ui/StructureExportPanel.d.ts.map +1 -1
  243. package/lib/structure/ui/StructureExportPanel.js +9 -4
  244. package/lib/structure/ui/StructureExportPanel.js.map +1 -1
  245. package/lib/structure/ui/index.d.ts +1 -0
  246. package/lib/structure/ui/index.d.ts.map +1 -1
  247. package/lib/structure/ui/index.js.map +1 -1
  248. package/package.json +4 -1
  249. package/src/card/core/cardAddress.ts +39 -3
  250. package/src/card/core/cardPageUrl.ts +10 -3
  251. package/src/card/core/index.ts +7 -1
  252. package/src/credits/core/credits.ts +40 -0
  253. package/src/download/core/downloadFigure.ts +26 -1
  254. package/src/download/core/figureDpi.ts +94 -0
  255. package/src/download/core/figurePng.ts +11 -2
  256. package/src/download/core/index.ts +13 -1
  257. package/src/download/core/pngDpi.ts +84 -0
  258. package/src/download/ui/FigureDownloadPanel.tsx +41 -51
  259. package/src/download/ui/figureDownloadHint.ts +13 -0
  260. package/src/download/ui/figureDownloadOptions.ts +85 -0
  261. package/src/download/ui/resolutionChoices.ts +67 -0
  262. package/src/download/ui/useFigureActions.ts +24 -9
  263. package/src/download/ui/useFigureDownload.tsx +57 -71
  264. package/src/ecosystem/core/sites.ts +14 -0
  265. package/src/ecosystem/core/types.ts +2 -1
  266. package/src/ecosystem/ui/glyphs.tsx +20 -0
  267. package/src/locales/de.json +30 -1
  268. package/src/locales/en.json +30 -1
  269. package/src/locales/es.json +30 -1
  270. package/src/locales/fr.json +30 -1
  271. package/src/locales/it.json +30 -1
  272. package/src/molecule3d/ui/Molecule3DStage.tsx +4 -88
  273. package/src/osiris/core/barGeometry.ts +74 -0
  274. package/src/osiris/core/computeProperties.ts +175 -0
  275. package/src/osiris/core/constants.ts +110 -0
  276. package/src/osiris/core/fitness.ts +59 -0
  277. package/src/osiris/core/index.ts +78 -0
  278. package/src/osiris/core/poolSize.ts +24 -0
  279. package/src/osiris/core/poolTypes.ts +140 -0
  280. package/src/osiris/core/predictIdCode.ts +69 -0
  281. package/src/osiris/core/predictProtocol.ts +78 -0
  282. package/src/osiris/core/predictionPool.ts +237 -0
  283. package/src/osiris/core/predictionQueue.ts +89 -0
  284. package/src/osiris/core/predictors.ts +142 -0
  285. package/src/osiris/core/propertyKeys.ts +54 -0
  286. package/src/osiris/core/riskAppearance.ts +86 -0
  287. package/src/osiris/core/riskDetail.ts +120 -0
  288. package/src/osiris/core/riskDetailRequest.ts +64 -0
  289. package/src/osiris/core/riskFindings.ts +92 -0
  290. package/src/osiris/core/runPredictionJob.ts +49 -0
  291. package/src/osiris/core/scales.ts +236 -0
  292. package/src/osiris/core/types.ts +103 -0
  293. package/src/osiris/core/valuation.ts +44 -0
  294. package/src/osiris/ui/PropertyBar.tsx +77 -0
  295. package/src/osiris/ui/PropertyPanel.tsx +227 -0
  296. package/src/osiris/ui/PropertyRows.tsx +92 -0
  297. package/src/osiris/ui/RiskFindings.tsx +137 -0
  298. package/src/osiris/ui/RiskPanel.tsx +81 -0
  299. package/src/osiris/ui/RiskSquare.tsx +108 -0
  300. package/src/osiris/ui/index.ts +17 -0
  301. package/src/osiris/ui/usePredictions.ts +194 -0
  302. package/src/osiris-ui.ts +11 -0
  303. package/src/osiris.ts +17 -0
  304. package/src/panel/ui/FilterInput.tsx +4 -1
  305. package/src/panel/ui/SidePanelItem.tsx +162 -0
  306. package/src/panel/ui/SidePanelStack.tsx +29 -36
  307. package/src/panel/ui/SpectrumColorSwatch.tsx +5 -3
  308. package/src/panel/ui/usePanelFolds.ts +103 -0
  309. package/src/shared/ui/FullscreenStage.tsx +96 -0
  310. package/src/shared/ui/canFillScreen.ts +8 -0
  311. package/src/shared/ui/index.ts +6 -0
  312. package/src/split/ui/splitPanes.tsx +4 -4
  313. package/src/structure/core/structureNotations.ts +25 -8
  314. package/src/structure/ui/StructureExportPanel.tsx +23 -4
  315. package/src/structure/ui/index.ts +4 -0
  316. package/styles/osiris.css +339 -0
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The resolution a PNG states, in its `pHYs` chunk.
3
+ *
4
+ * A browser writes none, and a word processor or a slide then places the
5
+ * picture as if it were 72 or 96 dpi — a figure painted for print arrives
6
+ * several times too large. Stated, it arrives at the size it was drawn.
7
+ */
8
+
9
+ import CRC32 from 'crc-32';
10
+
11
+ const SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
12
+
13
+ /** Metres in an inch: `pHYs` counts pixels per metre. */
14
+ const METRES_PER_INCH = 0.0254;
15
+
16
+ /**
17
+ * A PNG stating a resolution, any resolution it stated before dropped.
18
+ * @param png - The PNG file's bytes.
19
+ * @param dpi - Dots per inch.
20
+ * @returns A new PNG file.
21
+ * @throws {Error} When the bytes are not a PNG.
22
+ */
23
+ export function withPngDpi(
24
+ png: Uint8Array,
25
+ dpi: number,
26
+ ): Uint8Array<ArrayBuffer> {
27
+ for (let index = 0; index < SIGNATURE.length; index++) {
28
+ if (png[index] !== SIGNATURE[index]) throw new Error('Not a PNG file.');
29
+ }
30
+ const view = new DataView(png.buffer, png.byteOffset, png.byteLength);
31
+ const chunks: Uint8Array[] = [png.subarray(0, SIGNATURE.length)];
32
+ let offset = SIGNATURE.length;
33
+ while (offset + 8 <= png.length) {
34
+ const end = offset + 12 + view.getUint32(offset);
35
+ const type = chunkType(png, offset);
36
+ if (type !== 'pHYs') chunks.push(png.subarray(offset, end));
37
+ if (type === 'IHDR') chunks.push(physChunk(dpi));
38
+ offset = end;
39
+ }
40
+ let total = 0;
41
+ for (const chunk of chunks) total += chunk.length;
42
+ const result = new Uint8Array(total);
43
+ let position = 0;
44
+ for (const chunk of chunks) {
45
+ result.set(chunk, position);
46
+ position += chunk.length;
47
+ }
48
+ return result;
49
+ }
50
+
51
+ /**
52
+ * The resolution a PNG states.
53
+ * @param png - The PNG file's bytes.
54
+ * @returns Whole dots per inch, or `undefined` when it states none.
55
+ */
56
+ export function pngDpi(png: Uint8Array): number | undefined {
57
+ const view = new DataView(png.buffer, png.byteOffset, png.byteLength);
58
+ let offset = SIGNATURE.length;
59
+ while (offset + 8 <= png.length) {
60
+ if (chunkType(png, offset) === 'pHYs' && png[offset + 16] === 1) {
61
+ return Math.round(view.getUint32(offset + 8) * METRES_PER_INCH);
62
+ }
63
+ offset += 12 + view.getUint32(offset);
64
+ }
65
+ return undefined;
66
+ }
67
+
68
+ function chunkType(png: Uint8Array, offset: number): string {
69
+ return String.fromCodePoint(...png.subarray(offset + 4, offset + 8));
70
+ }
71
+
72
+ function physChunk(dpi: number): Uint8Array {
73
+ const chunk = new Uint8Array(21);
74
+ const view = new DataView(chunk.buffer);
75
+ view.setUint32(0, 9);
76
+ chunk.set([0x70, 0x48, 0x59, 0x73], 4);
77
+ const perMetre = Math.round(dpi / METRES_PER_INCH);
78
+ view.setUint32(8, perMetre);
79
+ view.setUint32(12, perMetre);
80
+ // The unit: 1 is the metre, 0 an aspect ratio with no size.
81
+ chunk[16] = 1;
82
+ view.setInt32(17, CRC32.buf(chunk.subarray(4, 17)));
83
+ return chunk;
84
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ReactElement, ReactNode } from 'react';
2
2
 
3
+ import { useLanguage } from '../../i18n/ui/useLanguage.ts';
3
4
  import { useChromeT } from '../../i18n/ui/useT.ts';
4
5
  import { OverlayAction } from '../../overlay/ui/OverlayAction.tsx';
5
6
  import { OverlayPanel } from '../../overlay/ui/OverlayPanel.tsx';
@@ -7,18 +8,14 @@ import type { OverlayOption } from '../../overlay/ui/OverlayRow.tsx';
7
8
  import { OverlaySegmented } from '../../overlay/ui/OverlaySegmented.tsx';
8
9
  import type { FigureNotice } from '../core/copyFigure.ts';
9
10
  import type { FigureFormat } from '../core/downloadFigure.ts';
11
+ import type { FigureResolutionUnit } from '../core/figureDpi.ts';
10
12
  import { figureLayoutSize } from '../core/figureLayout.ts';
11
13
  import type { FigurePixels } from '../core/figureScale.ts';
12
- import {
13
- figurePixels,
14
- figureScaleFits,
15
- figureScaleLabel,
16
- formatFigurePixels,
17
- } from '../core/figureScale.ts';
18
14
 
19
15
  import type { FigureSizing } from './FigureSizeRows.tsx';
20
16
  import { FigureSizeRows } from './FigureSizeRows.tsx';
21
17
  import { hintOf } from './figureDownloadHint.ts';
18
+ import { resolutionHelp, scaleChoices } from './resolutionChoices.ts';
22
19
 
23
20
  /** What {@link FigureDownloadPanel} is drawn from. */
24
21
  export interface FigureDownloadPanelProps {
@@ -30,6 +27,18 @@ export interface FigureDownloadPanelProps {
30
27
  scale: number;
31
28
  /** The multiples offered. */
32
29
  scales: readonly number[];
30
+ /**
31
+ * How the resolution reads: multiples of the figure on screen, or the dots
32
+ * per inch it has printed at its size on the page — 1× is 96 dpi.
33
+ * @default 'multiple'
34
+ */
35
+ unit?: FigureResolutionUnit;
36
+ /**
37
+ * Whether an SVG is saved at the size it states, whatever the resolution:
38
+ * the resolution then paints the PNG alone, and is not offered for the SVG.
39
+ * @default false
40
+ */
41
+ svgAtOwnSize?: boolean;
33
42
  /** How big the figure is on the page, or `null` where there is none. */
34
43
  size: FigurePixels | null;
35
44
  /** What went wrong the last time save was pressed, if anything. */
@@ -100,7 +109,9 @@ export function FigureDownloadPanel(
100
109
  const { title, format, scale, scales, size, failure, saving } = props;
101
110
  const { onFormatChange, onScaleChange, onSave, rasterSvg = false } = props;
102
111
  const { sizing, onCopy, notice = null, children } = props;
112
+ const { unit = 'multiple', svgAtOwnSize = false } = props;
103
113
  const t = useChromeT();
114
+ const language = useLanguage();
104
115
 
105
116
  // What the file is drawn at: the figure on screen, or the shape picked.
106
117
  const drawn =
@@ -109,13 +120,23 @@ export function FigureDownloadPanel(
109
120
  : figureLayoutSize(sizing.layout, size, sizing.custom ?? size);
110
121
 
111
122
  const painted = format === 'png' || rasterSvg;
123
+ const offerResolution = format === 'png' || !svgAtOwnSize;
112
124
 
113
125
  return (
114
126
  <OverlayPanel
115
127
  title={title}
116
- width={PANEL_WIDTH}
128
+ width={unit === 'dpi' ? DPI_PANEL_WIDTH : PANEL_WIDTH}
117
129
  hint={hintOf(
118
- { format, size: drawn, scale, failure, notice, rasterSvg },
130
+ {
131
+ format,
132
+ size: drawn,
133
+ scale,
134
+ failure,
135
+ notice,
136
+ rasterSvg,
137
+ unit,
138
+ language,
139
+ },
119
140
  t,
120
141
  )}
121
142
  actions={
@@ -152,20 +173,15 @@ export function FigureDownloadPanel(
152
173
  {sizing === undefined ? null : (
153
174
  <FigureSizeRows sizing={sizing} size={size} />
154
175
  )}
155
- <OverlaySegmented
156
- label={t('download.resolution')}
157
- help={{
158
- title: t('download.resolution'),
159
- body: t('download.resolutionHelp'),
160
- example: {
161
- code: '2×',
162
- note: t('download.resolutionExample'),
163
- },
164
- }}
165
- value={String(scale)}
166
- options={scaleChoices(scales, drawn, painted)}
167
- onChange={(picked) => onScaleChange(Number(picked))}
168
- />
176
+ {offerResolution ? (
177
+ <OverlaySegmented
178
+ label={t('download.resolution')}
179
+ help={resolutionHelp(unit, t)}
180
+ value={String(scale)}
181
+ options={scaleChoices(scales, drawn, painted, unit)}
182
+ onChange={(picked) => onScaleChange(Number(picked))}
183
+ />
184
+ ) : null}
169
185
  </OverlayPanel>
170
186
  );
171
187
  }
@@ -176,37 +192,11 @@ export function FigureDownloadPanel(
176
192
  */
177
193
  const PANEL_WIDTH = '27em';
178
194
 
195
+ /** Wide enough for the four dots per inch on one row, `1200 dpi` last. */
196
+ const DPI_PANEL_WIDTH = '33em';
197
+
179
198
  /** The two files a figure can leave the page as. */
180
199
  const FORMAT_CHOICES: ReadonlyArray<OverlayOption<FigureFormat>> = [
181
200
  { value: 'png', label: 'PNG' },
182
201
  { value: 'svg', label: 'SVG' },
183
202
  ];
184
-
185
- /**
186
- * The multiples offered, each saying what it would actually produce.
187
- *
188
- * One too large for the browser to paint is greyed rather than dropped, so the
189
- * reader learns the ceiling exists before they meet it as a blank file. An SVG
190
- * is not painted, so it has no such ceiling.
191
- * @param scales - The multiples the caller offers.
192
- * @param size - How big the figure is drawn for the file.
193
- * @param painted - Whether the file is painted into pixels.
194
- * @returns The choices, in the order offered.
195
- */
196
- function scaleChoices(
197
- scales: readonly number[],
198
- size: FigurePixels | null,
199
- painted: boolean,
200
- ): readonly OverlayOption[] {
201
- const choices: OverlayOption[] = [];
202
- for (const scale of scales) {
203
- const pixels = size === null ? null : figurePixels(size, scale);
204
- choices.push({
205
- value: String(scale),
206
- label: figureScaleLabel(scale),
207
- title: pixels === null ? undefined : formatFigurePixels(pixels),
208
- disabled: painted && size !== null && !figureScaleFits(size, scale),
209
- });
210
- }
211
- return choices;
212
- }
@@ -2,6 +2,8 @@ import type { ChromeKey } from '../../i18n/core/chromeCatalog.ts';
2
2
  import type { Translate } from '../../i18n/ui/useT.ts';
3
3
  import type { FigureNotice } from '../core/copyFigure.ts';
4
4
  import type { FigureFormat } from '../core/downloadFigure.ts';
5
+ import type { FigureResolutionUnit } from '../core/figureDpi.ts';
6
+ import { formatFigureCentimetres } from '../core/figureDpi.ts';
5
7
  import type { FigurePixels } from '../core/figureScale.ts';
6
8
  import { figurePixels, formatFigurePixels } from '../core/figureScale.ts';
7
9
 
@@ -19,6 +21,10 @@ export interface HintState {
19
21
  notice: FigureNotice | null;
20
22
  /** Whether the SVG embeds a rendered picture. */
21
23
  rasterSvg: boolean;
24
+ /** How the resolution is offered; in dpi, the printed size is written out. */
25
+ unit: FigureResolutionUnit;
26
+ /** The language of the page, which the printed size is written in. */
27
+ language: string;
22
28
  }
23
29
 
24
30
  /**
@@ -31,6 +37,7 @@ export interface HintState {
31
37
  */
32
38
  export function hintOf(state: HintState, t: Translate<ChromeKey>): string {
33
39
  const { format, size, scale, failure, notice, rasterSvg } = state;
40
+ const { unit, language } = state;
34
41
  if (failure !== null) return failure;
35
42
  if (size === null) return t('download.noFigure');
36
43
  if (notice === 'copyUnsupported') return t('download.copyUnsupported');
@@ -45,6 +52,12 @@ export function hintOf(state: HintState, t: Translate<ChromeKey>): string {
45
52
  });
46
53
  }
47
54
  const pixels = formatFigurePixels(figurePixels(size, scale));
55
+ if (unit === 'dpi') {
56
+ const printed = formatFigureCentimetres(size, language);
57
+ return format === 'svg'
58
+ ? t('download.hintSvgDpi', printed)
59
+ : t('download.hintPngDpi', { pixels, ...printed });
60
+ }
48
61
  return format === 'svg'
49
62
  ? t('download.hintSvg', { pixels })
50
63
  : t('download.hintPng', { pixels });
@@ -0,0 +1,85 @@
1
+ import type { FigureFormat } from '../core/downloadFigure.ts';
2
+ import type { FigureResolutionUnit } from '../core/figureDpi.ts';
3
+ import type { FigureSvg } from '../core/figureSvg.ts';
4
+
5
+ import type { FigureRenderer } from './useFigureRedraw.tsx';
6
+
7
+ /** What the "Save figure" panel is set up with. */
8
+ export interface FigureDownloadOptions {
9
+ /**
10
+ * The `id` of the box the figure is mounted in. Everything drawn inside it
11
+ * is saved — sixteen charts of a pair grid as readily as one scatter plot —
12
+ * and the controls floating over it are left behind. Give this or
13
+ * `writeFigure`.
14
+ * @default undefined
15
+ */
16
+ targetId?: string;
17
+ /**
18
+ * Writes the figure as an SVG document, in place of reading it off the
19
+ * page: for a drawing whose file carries more than its picture, such as the
20
+ * chemistry behind a structure. It is called when the panel opens, to
21
+ * measure the figure, and again for every save and copy. The SVG is saved as
22
+ * written, at the size it states; the resolution paints the PNG alone.
23
+ * @default undefined
24
+ */
25
+ writeFigure?: () => Promise<FigureSvg>;
26
+ /**
27
+ * What the saved file is called, without its extension. Name it after the
28
+ * data rather than after the tool: a reader with four of these in a
29
+ * downloads folder cannot tell four `figure.png` apart.
30
+ * @default 'figure'
31
+ */
32
+ fileName?: string;
33
+ /**
34
+ * The format it opens on.
35
+ * @default 'png'
36
+ */
37
+ defaultFormat?: FigureFormat;
38
+ /**
39
+ * How the resolution is offered: as multiples of the figure on screen, or —
40
+ * for a figure drawn to the size it prints at — in dots per inch. In dots
41
+ * per inch an SVG keeps the size it states, and 1× is 96 dpi.
42
+ * @default 'multiple'
43
+ */
44
+ unit?: FigureResolutionUnit;
45
+ /**
46
+ * The resolution a PNG opens on, as a multiple of the figure on screen:
47
+ * `dpiScale(600)` opens on 600 dpi. An SVG opens at 1×, the size it has on
48
+ * screen, and each format keeps the multiple the reader last picked for it.
49
+ * @default 2, or 300 dpi
50
+ */
51
+ defaultScale?: number;
52
+ /**
53
+ * The resolutions offered, as multiples of the figure on screen.
54
+ * @default [1, 2, 3, 4], or 150, 300, 600 and 1200 dpi
55
+ */
56
+ scales?: readonly number[];
57
+ /**
58
+ * Called with the multiple a PNG is set to, for a page that keeps the
59
+ * reader's choice — to paint its own PNGs at it, or to open on it next time.
60
+ * @default undefined
61
+ */
62
+ onScaleChange?: (scale: number) => void;
63
+ /**
64
+ * What is painted under the figure. `transparent` leaves it unpainted, for a
65
+ * figure going onto a coloured slide. A written figure paints its own.
66
+ * @default the surface the figure is drawn on
67
+ */
68
+ background?: string;
69
+ /**
70
+ * What the panel is called.
71
+ * @default the chrome's own line, in the language of the page
72
+ */
73
+ title?: string;
74
+ /**
75
+ * Draws the figure at a size, for a file. When it is given the panel offers
76
+ * a size — as shown, 4:3, 16:9, a journal column, or typed — and a figure
77
+ * saved at a size other than its own is drawn again at that size, off the
78
+ * page, so its axes and labels are laid out for the new shape rather than
79
+ * stretched into it. Draw exactly what is on screen, with the same data,
80
+ * zoom and colours; only the box changes. A written figure has the size it
81
+ * states, and is offered none.
82
+ * @default undefined — the figure is saved at the size it has on screen
83
+ */
84
+ renderFigure?: FigureRenderer;
85
+ }
@@ -0,0 +1,67 @@
1
+ import type { HelpContent } from '../../help/ui/HelpBody.tsx';
2
+ import type { ChromeKey } from '../../i18n/core/chromeCatalog.ts';
3
+ import type { Translate } from '../../i18n/ui/useT.ts';
4
+ import type { OverlayOption } from '../../overlay/ui/OverlayRow.tsx';
5
+ import type { FigureResolutionUnit } from '../core/figureDpi.ts';
6
+ import { figureResolutionLabel } from '../core/figureDpi.ts';
7
+ import type { FigurePixels } from '../core/figureScale.ts';
8
+ import {
9
+ figurePixels,
10
+ figureScaleFits,
11
+ formatFigurePixels,
12
+ } from '../core/figureScale.ts';
13
+
14
+ /**
15
+ * The resolutions offered, each saying what it would actually produce.
16
+ *
17
+ * One too large for the browser to paint is greyed rather than dropped, so the
18
+ * reader learns the ceiling exists before they meet it as a blank file. An SVG
19
+ * is not painted, so it has no such ceiling.
20
+ * @param scales - The multiples the caller offers.
21
+ * @param size - How big the figure is drawn for the file.
22
+ * @param painted - Whether the file is painted into pixels.
23
+ * @param unit - How the resolution reads.
24
+ * @returns The choices, in the order offered.
25
+ */
26
+ export function scaleChoices(
27
+ scales: readonly number[],
28
+ size: FigurePixels | null,
29
+ painted: boolean,
30
+ unit: FigureResolutionUnit,
31
+ ): readonly OverlayOption[] {
32
+ const choices: OverlayOption[] = [];
33
+ for (const scale of scales) {
34
+ const pixels = size === null ? null : figurePixels(size, scale);
35
+ choices.push({
36
+ value: String(scale),
37
+ label: figureResolutionLabel(scale, unit),
38
+ title: pixels === null ? undefined : formatFigurePixels(pixels),
39
+ disabled: painted && size !== null && !figureScaleFits(size, scale),
40
+ });
41
+ }
42
+ return choices;
43
+ }
44
+
45
+ /**
46
+ * What the resolution means, in the unit it is offered in.
47
+ * @param unit - How the resolution reads.
48
+ * @param t - The chrome's formatter.
49
+ * @returns The help of the row.
50
+ */
51
+ export function resolutionHelp(
52
+ unit: FigureResolutionUnit,
53
+ t: Translate<ChromeKey>,
54
+ ): HelpContent {
55
+ const title = t('download.resolution');
56
+ return unit === 'dpi'
57
+ ? {
58
+ title,
59
+ body: t('download.resolutionDpiHelp'),
60
+ example: { code: '300 dpi', note: t('download.resolutionDpiExample') },
61
+ }
62
+ : {
63
+ title,
64
+ body: t('download.resolutionHelp'),
65
+ example: { code: '2×', note: t('download.resolutionExample') },
66
+ };
67
+ }
@@ -1,8 +1,9 @@
1
1
  import { copyFigure } from '../core/copyFigure.ts';
2
2
  import type { FigureFormat } from '../core/downloadFigure.ts';
3
- import { downloadFigure } from '../core/downloadFigure.ts';
3
+ import { downloadFigure, downloadFigureSvg } from '../core/downloadFigure.ts';
4
4
  import { figurePng } from '../core/figurePng.ts';
5
5
  import type { FigurePixels } from '../core/figureScale.ts';
6
+ import type { FigureSvg } from '../core/figureSvg.ts';
6
7
  import { figureSvg } from '../core/figureSvg.ts';
7
8
 
8
9
  import type { FigureRedraw } from './useFigureRedraw.tsx';
@@ -12,7 +13,9 @@ import { useFigureTask } from './useFigureTask.ts';
12
13
  /** What {@link useFigureActions} works from. */
13
14
  export interface FigureActionsOptions {
14
15
  /** The `id` of the box the figure is mounted in. */
15
- targetId: string;
16
+ targetId?: string;
17
+ /** Writes the figure, in place of reading it off the page. */
18
+ writeFigure?: () => Promise<FigureSvg>;
16
19
  /** Which file save writes. */
17
20
  format: FigureFormat;
18
21
  /** The multiple of the figure on screen it is painted at. */
@@ -47,17 +50,26 @@ export interface FigureActions extends Omit<FigureTask, 'run'> {
47
50
  * @returns See {@link FigureActions}.
48
51
  */
49
52
  export function useFigureActions(options: FigureActionsOptions): FigureActions {
50
- const { targetId, format, scale, fileName, background } = options;
53
+ const { targetId, writeFigure, format, scale, fileName, background } =
54
+ options;
51
55
  const { redrawAt, redraw } = options;
52
56
  const { run, busy, failure, notice, clear } = useFigureTask();
53
57
 
54
- function withFigure(write: (box: string | Element) => Promise<void>) {
58
+ async function withFigure(write: (box: string | Element) => Promise<void>) {
59
+ if (targetId === undefined) throw new Error('There is no figure to save.');
55
60
  return redrawAt === null
56
61
  ? write(targetId)
57
62
  : redraw(targetId, redrawAt, write);
58
63
  }
59
64
 
60
65
  function save(): void {
66
+ if (writeFigure !== undefined) {
67
+ const written = writeFigure().then((figure) =>
68
+ downloadFigureSvg(figure, { format, scale, fileName }),
69
+ );
70
+ run(written.then(() => null));
71
+ return;
72
+ }
61
73
  const options = { format, scale, fileName, background };
62
74
  run(withFigure((box) => downloadFigure(box, options)).then(() => null));
63
75
  }
@@ -65,11 +77,14 @@ export function useFigureActions(options: FigureActionsOptions): FigureActions {
65
77
  function copy(): void {
66
78
  // The clipboard is written before anything is awaited, with the picture
67
79
  // still being painted: Safari only allows a write the click itself started.
68
- const png = new Promise<Blob>((resolve, reject) => {
69
- withFigure(async (box) => {
70
- resolve(await figurePng(figureSvg(box, { background }), scale));
71
- }).catch(reject);
72
- });
80
+ const png =
81
+ writeFigure === undefined
82
+ ? new Promise<Blob>((resolve, reject) => {
83
+ withFigure(async (box) => {
84
+ resolve(await figurePng(figureSvg(box, { background }), scale));
85
+ }).catch(reject);
86
+ })
87
+ : writeFigure().then((figure) => figurePng(figure, scale));
73
88
  run(copyFigure(png));
74
89
  }
75
90