@openpresentation/opf-pptx 0.16.0 → 0.16.1

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 (2) hide show
  1. package/dist/index.js +46 -2
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3171,13 +3171,57 @@ function slideTextMeasurement(presentation, index, options) {
3171
3171
  return chosenFamilyMeasurement(adapted);
3172
3172
  }
3173
3173
 
3174
+ // DrawingML has only a bold flag: a run written for the chosen family (no provider face such as Roboto SemiBold) draws that
3175
+ // family's bold face at 600 and above and its regular face below, whatever weight the engine asked for.
3176
+ const nativeDrawnWeight = weight => weight >= 600 ? 700 : 400;
3177
+ const FACE_WEIGHTS = [100, 200, 300, 400, 500, 600, 700, 800, 900];
3178
+ const styleName = (weight, italic) => `weight ${weight} ${italic ? 'italic' : 'upright'}`;
3179
+
3180
+ // RR-59 (opf-pptx#214): a provider's font-unavailable for a family whose faces are loaded, only not at the requested weight and
3181
+ // style, names the family, weight and style that are missing and the ones that are loaded. Any other error is returned unchanged.
3182
+ function missingFaceError(error, style, measurement, drawn) {
3183
+ if (error?.code !== 'font-unavailable' || typeof style?.fontFamily !== 'string') return error;
3184
+ const weight = style.fontWeight ?? 400, italic = !!style.italic, loaded = [];
3185
+ for (const candidateItalic of [false, true]) for (const candidate of FACE_WEIGHTS) {
3186
+ if (candidate === weight && candidateItalic === italic) continue;
3187
+ try { const face = measurement.resolveStyle({...style, fontWeight: candidate, italic: candidateItalic}); loaded.push({weight: candidate, italic: candidateItalic, family: face?.fontFace?.family ?? face?.fontFamily}); }
3188
+ catch { /* not loaded at this weight and style */ }
3189
+ }
3190
+ if (!loaded.length) return error;
3191
+ const policy = error.details?.substitutionPolicy, replacement = error.details?.replacement;
3192
+ const families = [...new Set(loaded.map(face => face.family).filter(Boolean))], wanted = styleName(drawn ?? weight, italic);
3193
+ const message = `No local font face for '${style.fontFamily}' at ${styleName(weight, italic)}${policy ? ` under substitutionPolicy '${policy}'` : ''}`
3194
+ + `${drawn !== undefined && drawn !== weight ? `, nor at ${wanted}, the face PowerPoint draws the run with` : ''}. '${style.fontFamily}' resolves only at ${loaded.map(face => styleName(face.weight, face.italic)).join(', ')}`
3195
+ + `${families.length ? ` (${families.join(', ')})` : ''}. Load ${typeof replacement === 'string' ? `its replacement '${replacement}' or licensed '${style.fontFamily}' files` : `a '${style.fontFamily}' face`} at ${wanted} (loadFonts({faces})).`;
3196
+ const Type = typeof error.constructor === 'function' && error.constructor !== Error ? error.constructor : undefined;
3197
+ const details = {...error.details, fontFamily: style.fontFamily, fontWeight: weight, italic, loadedStyles: loaded.map(({weight, italic}) => ({weight, italic})), cause: error.message};
3198
+ let improved;
3199
+ try { improved = Type ? new Type('font-unavailable', message, details) : undefined; } catch { improved = undefined; }
3200
+ if (!(improved instanceof Error) || improved.code !== 'font-unavailable') improved = Object.assign(new Error(message), {name: error.name, code: 'font-unavailable', details});
3201
+ improved.cause = error;
3202
+ return improved;
3203
+ }
3204
+
3174
3205
  function chosenFamilyMeasurement(measurement) {
3175
3206
  if (!measurement || typeof measurement.resolveStyle !== 'function') return measurement;
3176
3207
  const resolveFont = typeof measurement.resolveFont === 'function' ? style => measurement.resolveFont(style) : undefined;
3177
3208
  const wrapped = {
3178
3209
  measure: (text, size, style) => measurement.measure(text, size, style),
3179
- resolveStyle: style => {
3180
- const resolved = measurement.resolveStyle(style);
3210
+ resolveStyle: input => {
3211
+ // RR-59 (opf-pptx#214): engine-chosen weights (the image placeholder label and media caption at 600, core's metric value at
3212
+ // 800, metric labels and timeline event text at 500) are not always loaded: a metric replacement holds at 400 and 700 only
3213
+ // (Aptos -> Intos). The run is written for the chosen family with a bold flag, so it is measured at the weight PowerPoint
3214
+ // draws it in, which the policy does resolve. A family the policy cannot resolve at that weight either still fails, and
3215
+ // a style that resolves keeps its own resolution, so no output that exported before changes.
3216
+ let style = input, resolved;
3217
+ try { resolved = measurement.resolveStyle(style); }
3218
+ catch (error) {
3219
+ const weight = style?.fontWeight ?? 400, drawn = nativeDrawnWeight(weight);
3220
+ if (error?.code !== 'font-unavailable' || drawn === weight) throw missingFaceError(error, style, measurement);
3221
+ style = {...style, fontWeight: drawn};
3222
+ try { resolved = measurement.resolveStyle(style); }
3223
+ catch { throw missingFaceError(error, input, measurement, drawn); }
3224
+ }
3181
3225
  const requested = style?.fontFamily;
3182
3226
  // Theme tokens are provider business; the exporter always passes concrete families.
3183
3227
  if (typeof requested !== 'string' || requested.startsWith('+')) return resolved;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-pptx",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Pure local OPF to PPTX export and PPTX to OPF import tooling.",
5
5
  "license": "MIT",
6
6
  "type": "module",