1 package org.daisy.pipeline.braille.dotify.calabash.impl;
2
3 import java.io.ByteArrayInputStream;
4 import java.io.ByteArrayOutputStream;
5 import java.io.InputStream;
6 import java.lang.reflect.InvocationTargetException;
7 import java.util.ArrayList;
8 import java.util.HashMap;
9 import java.util.List;
10 import java.util.Map;
11 import java.util.NoSuchElementException;
12
13 import javax.xml.transform.stream.StreamSource;
14
15 import com.google.common.collect.Iterables;
16
17 import com.xmlcalabash.core.XProcException;
18 import com.xmlcalabash.core.XProcRuntime;
19 import com.xmlcalabash.io.ReadablePipe;
20 import com.xmlcalabash.io.WritablePipe;
21 import com.xmlcalabash.library.DefaultStep;
22 import com.xmlcalabash.model.RuntimeValue;
23 import com.xmlcalabash.runtime.XAtomicStep;
24
25 import net.sf.saxon.s9api.QName;
26 import net.sf.saxon.s9api.SaxonApiException;
27 import net.sf.saxon.s9api.Serializer;
28 import net.sf.saxon.s9api.XdmNode;
29
30 import org.daisy.braille.css.InlineStyle;
31 import org.daisy.braille.css.RuleCounterStyle;
32 import org.daisy.common.xproc.calabash.XProcStep;
33 import org.daisy.common.xproc.calabash.XProcStepProvider;
34 import org.daisy.common.xproc.XProcMonitor;
35 import org.daisy.dotify.api.engine.FormatterEngine;
36 import org.daisy.dotify.api.engine.FormatterEngineFactoryService;
37 import org.daisy.dotify.api.formatter.FormatterConfiguration;
38 import org.daisy.dotify.api.formatter.FormatterFactory;
39 import org.daisy.dotify.api.obfl.ObflParserFactoryService;
40 import org.daisy.dotify.api.table.BrailleConverter;
41 import org.daisy.dotify.api.table.Table;
42 import org.daisy.dotify.api.translator.TextBorderConfigurationException;
43 import org.daisy.dotify.api.translator.TextBorderFactory;
44 import org.daisy.dotify.api.translator.TextBorderFactoryMakerService;
45 import org.daisy.dotify.api.translator.TextBorderFactoryService;
46 import org.daisy.dotify.api.translator.TextBorderStyle;
47 import org.daisy.dotify.api.writer.MetaDataItem;
48 import org.daisy.dotify.api.writer.PagedMediaWriter;
49 import org.daisy.dotify.api.writer.PagedMediaWriterConfigurationException;
50 import org.daisy.pipeline.braille.common.BrailleTranslator;
51 import org.daisy.pipeline.braille.common.BrailleTranslatorRegistry;
52 import org.daisy.pipeline.braille.common.Query;
53 import static org.daisy.pipeline.braille.common.Query.util.mutableQuery;
54 import static org.daisy.pipeline.braille.common.Query.util.query;
55 import static org.daisy.pipeline.braille.common.TransformProvider.util.logCreate;
56 import org.daisy.pipeline.braille.common.util.Function0;
57 import org.daisy.pipeline.braille.common.util.Functions;
58 import org.daisy.pipeline.braille.dotify.impl.CounterHandlingBrailleTranslator;
59 import org.daisy.pipeline.braille.pef.TableRegistry;
60 import org.daisy.pipeline.css.CounterStyle;
61
62 import org.osgi.framework.FrameworkUtil;
63
64 import org.osgi.service.component.annotations.Activate;
65 import org.osgi.service.component.annotations.Component;
66 import org.osgi.service.component.annotations.Reference;
67 import org.osgi.service.component.annotations.ReferenceCardinality;
68 import org.osgi.service.component.annotations.ReferencePolicy;
69
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
72
73
74
75
76
77
78
79 public class OBFLToPEFStep extends DefaultStep implements XProcStep {
80
81 private static final QName _locale = new QName("locale");
82 private static final QName _mode = new QName("mode");
83 private static final QName _braille_charset = new QName("braille-charset");
84 private static final QName _identifier = new QName("identifier");
85 private static final QName _style_type = new QName("style-type");
86 private static final QName _css_text_transform_definitions = new QName("css-text-transform-definitions");
87 private static final QName _css_hyphenation_resource_definitions = new QName("css-hyphenation-resource-definitions");
88 private static final QName _css_counter_style_definitions = new QName("css-counter-style-definitions");
89 private static final QName _has_volume_transition = new QName("has-volume-transition");
90
91 private static final String ERR_NS = "http://www.daisy.org/ns/pipeline/errors";
92
93
94
95
96 private static final QName ERR_DOTIFY_001 = new QName("pe", ERR_NS, "DOTIFY001");
97
98
99
100
101 private static final QName ERR_DOTIFY_002 = new QName("pe", ERR_NS, "DOTIFY002");
102
103
104
105
106 private static final QName ERR_DOTIFY_003 = new QName("pe", ERR_NS, "DOTIFY003");
107
108 private ReadablePipe source = null;
109 private WritablePipe result = null;
110 private final Map<String,String> params = new HashMap<String,String>();
111
112 private final FormatterEngineFactoryService formatterEngineFactoryService;
113 private final FormatterFactory formatterFactory;
114 private final ObflParserFactoryService obflParserFactoryService;
115 private final TextBorderFactoryService textBorderFactoryService;
116 private final BrailleTranslatorRegistry brailleTranslatorRegistry;
117 private final TableRegistry tableRegistry;
118 private final TemporaryBrailleTranslatorProvider temporaryBrailleTranslatorProvider;
119
120 public OBFLToPEFStep(XProcRuntime runtime,
121 XAtomicStep step,
122 FormatterEngineFactoryService formatterEngineFactoryService,
123 FormatterFactory formatterFactory,
124 ObflParserFactoryService obflParserFactoryService,
125 TextBorderFactoryService textBorderFactoryService,
126 BrailleTranslatorRegistry brailleTranslatorRegistry,
127 TableRegistry tableRegistry,
128 TemporaryBrailleTranslatorProvider temporaryBrailleTranslatorProvider) {
129 super(runtime, step);
130 this.formatterEngineFactoryService = formatterEngineFactoryService;
131 this.formatterFactory = formatterFactory;
132 this.obflParserFactoryService = obflParserFactoryService;
133 this.textBorderFactoryService = textBorderFactoryService;
134 this.brailleTranslatorRegistry = brailleTranslatorRegistry;
135 this.tableRegistry = tableRegistry;
136 if (temporaryBrailleTranslatorProvider == null) throw new IllegalStateException();
137 this.temporaryBrailleTranslatorProvider = temporaryBrailleTranslatorProvider;
138 }
139
140 @Override
141 public void setInput(String port, ReadablePipe pipe) {
142 source = pipe;
143 }
144
145 @Override
146 public void setOutput(String port, WritablePipe pipe) {
147 result = pipe;
148 }
149
150 @Override
151 public void setParameter(String port, QName name, RuntimeValue value) {
152 if ("parameters".equals(port))
153 setParameter(name, value);
154 else
155 throw new XProcException("No parameters allowed on port '" + port + "'");
156 }
157
158 @Override
159 public void setParameter(QName name, RuntimeValue value) {
160 if ("".equals(name.getNamespaceURI()))
161 params.put(name.getLocalName(), value.getString());
162 }
163
164 @Override
165 public void reset() {
166 source.resetReader();
167 result.resetWriter();
168 }
169
170 @Override
171 public void run() throws SaxonApiException {
172 super.run();
173 Function0<Void> evictTempTranslator = Functions.noOp;
174
175 try {
176
177 XdmNode obflNode = source.read();
178 String styleType = getOption(_style_type, "");
179 String mode = getOption(_mode).getString();
180 String locale = getOption(_locale).getString();
181 String brailleCharset = getOption(_braille_charset, "");
182
183 if (brailleCharset != null && !"".equals(brailleCharset)) {
184
185 Query modeQuery;
186 try {
187 modeQuery = query(mode);
188 } catch (Exception e) {
189 throw new IllegalArgumentException("Expected mode in query format: " + mode, e);
190 }
191 mode = mutableQuery(modeQuery).add("braille-charset", brailleCharset).toString();
192 }
193
194 if ("text/css".equals(styleType)) {
195
196
197
198 Query mainQuery;
199 try {
200 mainQuery = query(mode);
201 } catch (Exception e) {
202 throw new IllegalArgumentException("Expected mode in query format: " + mode, e);
203 }
204 mainQuery = mutableQuery(mainQuery).add("input", "text-css").add("output", "braille");
205 mode = mainQuery.toString();
206 if (locale != null && !"und".equals(locale))
207 mainQuery = mutableQuery(mainQuery).add("document-locale", locale);
208 BrailleTranslator translator; {
209 String textTransformAndHyphenationResourceDefinitions
210 = getOption(_css_text_transform_definitions, "")
211 + getOption(_css_hyphenation_resource_definitions, "");
212 try {
213 translator = (!"".equals(textTransformAndHyphenationResourceDefinitions)
214 ? brailleTranslatorRegistry.getWithHyphenator(mainQuery,
215 textTransformAndHyphenationResourceDefinitions,
216 obflNode.getBaseURI(),
217
218 false)
219 : brailleTranslatorRegistry.getWithHyphenator(mainQuery)
220 ).iterator().next();
221 } catch (NoSuchElementException e) {
222 if (!"".equals(textTransformAndHyphenationResourceDefinitions))
223 throw new XProcException(
224 "No translator available for style type '" + styleType + "', mode '" + mode
225 + "', locale '" + locale + "' and CSS rules:\n" + textTransformAndHyphenationResourceDefinitions);
226 else
227 throw new XProcException(
228 step.getNode(),
229 "No translator available for style type '" + styleType + "', mode '" + mode
230 + "' and locale '" + locale + "'");
231 }
232 }
233 String counterStyleDefinitions = getOption(_css_counter_style_definitions, "");
234 Map<String,CounterStyle> counterStyles = "".equals(counterStyleDefinitions)
235 ? null
236 : CounterStyle.parseCounterStyleRules(
237 Iterables.filter(new InlineStyle(counterStyleDefinitions), RuleCounterStyle.class));
238
239 translator = new CounterHandlingBrailleTranslator(translator, counterStyles);
240 translator = logCreate(translator, logger);
241 evictTempTranslator = temporaryBrailleTranslatorProvider.provideTemporarily(translator);
242 mode = mutableQuery().add("id", translator.getIdentifier()).toString();
243 locale = "und";
244 } else if (!"".equals(styleType)) {
245 throw new XProcException(step.getNode(), "Value of style-type option not recognized: " + styleType);
246 }
247
248
249 ByteArrayOutputStream s = new ByteArrayOutputStream();
250 Serializer serializer = runtime.getProcessor().newSerializer();
251 serializer.setOutputStream(s);
252 serializer.setCloseOnCompletion(true);
253 serializer.serializeNode(obflNode);
254 serializer.close();
255 InputStream obflStream = new ByteArrayInputStream(s.toByteArray());
256 s.close();
257
258
259 String identifier = getOption(_identifier, "");
260 boolean markCapitalLetters; {
261 String p = params.get("mark-capital-letters");
262 markCapitalLetters = (p == null) ? true : !p.equalsIgnoreCase("false"); }
263 boolean hyphenate; {
264 String p = params.get("hyphenate");
265 hyphenate = (p == null) ? true : !p.equalsIgnoreCase("false"); }
266 boolean allowTextOverflowTrimming; {
267 String p = params.get("allow-text-overflow-trimming");
268 allowTextOverflowTrimming = (p == null) ? false : p.equalsIgnoreCase("true"); }
269 boolean removeStyles; {
270 String p = params.get("remove-styles");
271 removeStyles = (p == null) ? false : p.equalsIgnoreCase("true"); }
272 boolean allowEndingPageOnHyphen;
273 boolean allowEndingVolumeOnHyphen; {
274 String p = params.get("hyphenation-at-page-breaks");
275 if ("false".equalsIgnoreCase(p)) {
276 allowEndingPageOnHyphen = false;
277 allowEndingVolumeOnHyphen = false;
278 } else if ("except-at-volume-breaks".equalsIgnoreCase(p)) {
279 allowEndingPageOnHyphen = true;
280 allowEndingVolumeOnHyphen = false;
281 } else {
282 allowEndingPageOnHyphen = true;
283
284
285
286
287
288
289 allowEndingVolumeOnHyphen = !getOption(_has_volume_transition, false);
290 }
291 }
292 FormatterConfiguration.Builder config = FormatterConfiguration.with(locale, mode)
293 .markCapitalLetters(markCapitalLetters)
294 .hyphenate(hyphenate)
295 .allowsTextOverflowTrimming(allowTextOverflowTrimming)
296 .allowsEndingPageOnHyphen(allowEndingPageOnHyphen)
297 .allowsEndingVolumeOnHyphen(allowEndingVolumeOnHyphen);
298 if (removeStyles)
299 config.ignoreStyle("em").ignoreStyle("strong");
300 Table brailleCharsetTable = "".equals(brailleCharset)
301 ? null
302 : tableRegistry.get(mutableQuery().add("id", brailleCharset)).iterator().next();
303 FormatterEngine engine = newFormatterEngine(config.build(),
304 newPEFWriter(identifier, brailleCharsetTable),
305 brailleCharsetTable == null
306 ? null
307 : brailleCharsetTable.newBrailleConverter());
308 s = new ByteArrayOutputStream();
309 engine.convert(obflStream, s);
310 obflStream.close();
311 InputStream pefStream = new ByteArrayInputStream(s.toByteArray());
312 s.close();
313
314
315 result.write(runtime.getProcessor().newDocumentBuilder().build(new StreamSource(pefStream)));
316 pefStream.close(); }
317
318 catch (Throwable e) {
319 String msg = e.getMessage();
320 if (msg != null)
321 if ((msg.contains("Cannot fit") && msg.contains("into a margin-region of size"))
322 || msg.contains("Failed to solve table"))
323 throw new XProcException(ERR_DOTIFY_002, step, e);
324 throw XProcStep.raiseError(e, step); }
325 finally {
326 evictTempTranslator.apply(); }
327 }
328
329 private PagedMediaWriter newPEFWriter(String identifier, Table brailleCharset) throws PagedMediaWriterConfigurationException {
330 PagedMediaWriter writer = null;
331 List<MetaDataItem> meta = new ArrayList<MetaDataItem>();
332 if (brailleCharset != null)
333 meta.add(new MetaDataItem(new javax.xml.namespace.QName("http://www.daisy.org/ns/pipeline/",
334 "ascii-braille-charset",
335 "dp2"),
336 brailleCharset.getIdentifier()));
337 if (!"".equals(identifier))
338 meta.add(new MetaDataItem(new javax.xml.namespace.QName("http://purl.org/dc/elements/1.1/", "identifier", "dc"),
339 identifier));
340 writer = new PEFWriter(brailleCharset);
341 if (!meta.isEmpty())
342 writer.prepare(meta);
343 return writer;
344 }
345
346 private FormatterEngine newFormatterEngine(FormatterConfiguration config,
347 PagedMediaWriter writer,
348 BrailleConverter brailleCharset) {
349
350
351
352
353
354
355
356
357
358
359
360
361 TextBorderFactoryMakerService asciiTextBorderFactoryMakerService = new TextBorderFactoryMakerService() {
362 public TextBorderStyle newTextBorderStyle(Map<String,Object> features)
363 throws TextBorderConfigurationException {
364 TextBorderFactory f = textBorderFactoryService.newFactory();
365 for (String k : features.keySet())
366 f.setFeature(k, features.get(k));
367 TextBorderStyle style = f.newTextBorderStyle();
368 if (brailleCharset == null)
369 return style;
370 return new TextBorderStyle.Builder()
371 .topLeftCorner (brailleCharset.toText(style.getTopLeftCorner()))
372 .topBorder (brailleCharset.toText(style.getTopBorder()))
373 .topRightCorner (brailleCharset.toText(style.getTopRightCorner()))
374 .leftBorder (brailleCharset.toText(style.getLeftBorder()))
375 .rightBorder (brailleCharset.toText(style.getRightBorder()))
376 .bottomLeftCorner (brailleCharset.toText(style.getBottomLeftCorner()))
377 .bottomBorder (brailleCharset.toText(style.getBottomBorder()))
378 .bottomRightCorner(brailleCharset.toText(style.getBottomRightCorner()))
379 .build();
380 }
381 };
382 for (Object object : new Object[]{formatterEngineFactoryService,
383 formatterFactory,
384 obflParserFactoryService}) {
385 try {
386 object.getClass()
387 .getMethod(object == formatterFactory ? "setTextBorderFactory" : "setTextBorderFactoryMaker",
388 TextBorderFactoryMakerService.class)
389 .invoke(object, asciiTextBorderFactoryMakerService);
390 } catch (NoSuchMethodException |
391 SecurityException |
392 IllegalAccessException |
393 IllegalArgumentException |
394 InvocationTargetException e) {
395 throw new RuntimeException(e);
396 }
397 }
398 return formatterEngineFactoryService.newFormatterEngine(config, writer);
399 }
400
401 @Component(
402 name = "pxi:obfl-to-pef",
403 service = { XProcStepProvider.class },
404 property = { "type:String={http://www.daisy.org/ns/pipeline/xproc/internal}obfl-to-pef" }
405 )
406 public static class Provider implements XProcStepProvider {
407
408 @Override
409 public XProcStep newStep(XProcRuntime runtime, XAtomicStep step, XProcMonitor monitor, Map<String,String> properties) {
410 return new OBFLToPEFStep(runtime,
411 step,
412 formatterEngineFactoryService,
413 formatterFactory,
414 obflParserFactoryService,
415 textBorderFactoryService,
416 brailleTranslatorRegistry,
417 tableRegistry,
418 temporaryBrailleTranslatorProvider);
419 }
420
421 @Activate
422 protected void connectDotifyServices() {
423 for (Object object : new Object[]{formatterEngineFactoryService,
424 obflParserFactoryService}) {
425 try {
426 object.getClass()
427 .getMethod("setFormatterFactory", FormatterFactory.class)
428 .invoke(object, formatterFactory);
429 } catch (NoSuchMethodException |
430 SecurityException |
431 IllegalAccessException |
432 IllegalArgumentException |
433 InvocationTargetException e) {
434 throw new RuntimeException(e);
435 }
436 }
437 try {
438 formatterEngineFactoryService.getClass()
439 .getMethod("setObflParserFactory", ObflParserFactoryService.class)
440 .invoke(formatterEngineFactoryService, obflParserFactoryService);
441 } catch (NoSuchMethodException |
442 SecurityException |
443 IllegalAccessException |
444 IllegalArgumentException |
445 InvocationTargetException e) {
446 throw new RuntimeException(e);
447 }
448 }
449
450 private FormatterEngineFactoryService formatterEngineFactoryService;
451
452 @Reference(
453 name = "FormatterEngineFactoryService",
454 unbind = "-",
455 service = FormatterEngineFactoryService.class,
456 cardinality = ReferenceCardinality.MANDATORY,
457 policy = ReferencePolicy.STATIC
458 )
459 protected void bindFormatterEngineFactoryService(FormatterEngineFactoryService service) {
460 if (!OSGiHelper.inOSGiContext())
461 service.setCreatedWithSPI();
462 formatterEngineFactoryService = service;
463 }
464
465 private FormatterFactory formatterFactory;
466
467 @Reference(
468 name = "FormatterFactory",
469 unbind = "-",
470 service = FormatterFactory.class,
471 cardinality = ReferenceCardinality.MANDATORY,
472 policy = ReferencePolicy.STATIC
473 )
474 protected void bindFormatterFactory(FormatterFactory factory) {
475 if (!OSGiHelper.inOSGiContext())
476 factory.setCreatedWithSPI();
477 formatterFactory = factory;
478 }
479
480 private ObflParserFactoryService obflParserFactoryService;
481
482 @Reference(
483 name = "ObflParserFactoryService",
484 unbind = "-",
485 service = ObflParserFactoryService.class,
486 cardinality = ReferenceCardinality.MANDATORY,
487 policy = ReferencePolicy.STATIC
488 )
489 protected void bindObflParserFactoryService(ObflParserFactoryService service) {
490 if (!OSGiHelper.inOSGiContext())
491 service.setCreatedWithSPI();
492 obflParserFactoryService = service;
493 }
494
495 private TextBorderFactoryService textBorderFactoryService;
496
497 @Reference(
498 name = "TextBorderFactoryService",
499 unbind = "-",
500 service = TextBorderFactoryService.class,
501 cardinality = ReferenceCardinality.MANDATORY,
502 policy = ReferencePolicy.STATIC
503 )
504 protected void bindTextBorderFactoryService(TextBorderFactoryService service) {
505 if (!OSGiHelper.inOSGiContext())
506 service.setCreatedWithSPI();
507 textBorderFactoryService = service;
508 }
509
510 private BrailleTranslatorRegistry brailleTranslatorRegistry;
511
512 @Reference(
513 name = "BrailleTranslatorRegistry",
514 unbind = "-",
515 service = BrailleTranslatorRegistry.class,
516 cardinality = ReferenceCardinality.MANDATORY,
517 policy = ReferencePolicy.STATIC
518 )
519 protected void bindBrailleTranslatorRegistry(BrailleTranslatorRegistry registry) {
520 brailleTranslatorRegistry = registry.withContext(logger);
521 logger.debug("Binding BrailleTranslator registry: {}", registry);
522 }
523
524 private TemporaryBrailleTranslatorProvider temporaryBrailleTranslatorProvider = null;
525
526 @Reference(
527 name = "TemporaryBrailleTranslatorProvider",
528 unbind = "-",
529 service = TemporaryBrailleTranslatorProvider.class,
530 cardinality = ReferenceCardinality.MANDATORY,
531 policy = ReferencePolicy.STATIC
532 )
533 protected void bindTemporaryBrailleTranslatorProvider(TemporaryBrailleTranslatorProvider provider) {
534 temporaryBrailleTranslatorProvider = provider;
535 }
536
537 private TableRegistry tableRegistry;
538
539 @Reference(
540 name = "TableRegistry",
541 unbind = "-",
542 service = TableRegistry.class,
543 cardinality = ReferenceCardinality.MANDATORY,
544 policy = ReferencePolicy.STATIC
545 )
546 protected void bindTableRegistry(TableRegistry registry) {
547 tableRegistry = registry;
548 }
549 }
550
551 private static final Logger logger = LoggerFactory.getLogger(OBFLToPEFStep.class);
552
553 private static abstract class OSGiHelper {
554 static boolean inOSGiContext() {
555 try {
556 return FrameworkUtil.getBundle(OSGiHelper.class) != null;
557 } catch (NoClassDefFoundError e) {
558 return false;
559 }
560 }
561 }
562 }