Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[SnowOne 2024] Константин Воливач: "DOCX и PDF: эволюция экспорта документов в PropTech"

[SnowOne 2024] Константин Воливач: "DOCX и PDF: эволюция экспорта документов в PropTech"

Если на вашем проекте есть формирование docx и pdf, то приходите послушать историю как мы сделали экспорт в pdf с помощью chrome и как шаблонизировали docx под каждого клиента.

В первой части доклада рассмотрим разные способы превращения xml в pdf, сделаем сервис экспорта, рассмотрим различные боли с которыми может столкнуться каждый, а также посмеёмся над факапами. Расскажу о том, как мы оформили этот процесс максимально гибко, из-за чего получили приятный бонус для бизнеса в будущем.

Во второй части доклада посмотрим, как сформированный подход распространить на docx и шаблонизировать его под клиентов, а также сложности, с которыми можно столкнуться, формируя красивые документы в docx формате. Сравнения с различными библиотеками прилагается.

jugnsk

May 01, 2024
Tweet

More Decks by jugnsk

Other Decks in Programming

Transcript

  1. План доклада Предметная область 1 2 3 4 5 6

    7 Поиск решения XML->PDF Шаблоны HTML Поиск решения HTML->PDF CDP4K Скриншоты Сервис печати 8 Акты (DOCX) 3
  2. Определение XSLT XSLT (Extensible Stylesheet Language Transformations) is a language

    originally designed for transforming XML documents into other XML documents, or other formats such as HTML for web pages, plain text or XSL Formatting Objects, which may subsequently be converted to other formats, such as PDF, PostScript and PNG. Support for JSON and plain-text transformation was added in later updates to the XSLT 1.0 specification. 18
  3. Пример XSL 19 <?xml version="1.0" encoding="utf-8"?> <xsl:stylesheet xmlns:xsl="http:</www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:template

    name="GetAssignationName"> <xsl:param name="AssignationCode"<> <xsl:choose> <xsl:when test="$AssignationCode='005001000000'"> <xsl:value-of select="'Жилое'"<> </xsl:when> <xsl:when test="$AssignationCode='005001000000'"> <xsl:value-of select="'Жилое'"<> </xsl:when> <xsl:when test="$AssignationCode='005001001000'"> <xsl:value-of select="'Постоянного проживания'"<> </xsl:when> <xsl:when test="$AssignationCode='005001001001'"> <xsl:value-of select="'Общежитие'"<> </xsl:when> <xsl:when test="$AssignationCode='005001002000'"> <xsl:value-of select="'Временного проживания'"<> </xsl:when>
  4. Пример трансформации с fop java 20 public void createPdfFile(String xmlDataFile,

    String templateFile, OutputStream pdfOutputStream) throws …{ System.out.println("Create pdf file <<."); File tempFile = File.createTempFile("fop-" + System.currentTimeMillis(), ".pdf"); </ holds references to configuration information and cached data </ reuse this instance if you plan to render multiple documents FopFactory fopFactory = FopFactory.newInstance(new File(".").toURI()); FOUserAgent userAgent = fopFactory.newFOUserAgent();
  5. Пример трансформации с fop java 21 try { </ set

    output format Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, userAgent, pdfOutputStream); </ Load template TransformerFactory transformerFactory = TransformerFactory.newInstance(); Transformer transformer = transformerFactory.newTransformer(new StreamSource(new File(templateFile))); </ Set value of parameters in stylesheet transformer.setParameter("version", "1.0"); </ Input for XSLT transformations Source xmlSource = new StreamSource(new File(xmlDataFile)); Result result = new SAXResult(fop.getDefaultHandler()); transformer.transform(xmlSource, result); } finally { tempFile.delete(); } }
  6. 4 Выводы по fop XSLT должны быть в fo формате

    Нет поддержки HTML->PDF 1 2 3 Хорошо конвертирует XML-PDF 22 В контуре использует 3 команды
  7. Как использовать библиотеку 27 private suspend fun printInternal(xmlPath: Path, xmlScheme:

    XmlScheme,destPath: Path) { val xslPath = xmlScheme.scheme val transformerPool = transformersByXsl.computeIfAbsent(xslPath) { xslClassPath -> val producer = TransformerProducer(xslClassPath) Pools.simpleFIFO(producer) } val html = transformerPool.acquireHandle().use { poolHandle -> val transformer = poolHandle.resource() val outputStream = ByteArrayOutputStream() val streamResult = StreamResult(outputStream) val source = StreamSource(XmlReader.read(xmlPath)) try { transformer.transform(source, streamResult) } finally { transformer.reset() } String(outputStream.toByteArray()) } }
  8. Работал за время ~130 мс по 99 перцентилю за 11

    месяцев За год 11.3 млн преобразований, надёжность saxon проверена хорошо 1 2 3 Выводы по Saxon Осталось решить задачу HTML->PDF 30
  9. Mustache 34 Logic-less templates. Available in Ruby, JavaScript, Python, Erlang,

    Elixir, PHP, Perl, Raku, Objective-C, Java, C#/.NET, Android, C++, CFEngine, Go, Lua, ooc, ActionScript, ColdFusion, Scala, Clojure[Script], Clojure, Fantom, CoffeeScript, D, Haskell, XQuery, ASP, Io, Dart, Haxe, Delphi, Racket, Rust, OCaml, Swift, Bash, Julia, R, Crystal, Common Lisp, Nim, Pharo, Tcl, C, ABAP, Elm, Kotlin, SQL, PowerShell, and for Odin Works great with TextMate, Vim, Emacs, Coda, and Atom The Manual: mustache and mustache
  10. Второй способ используем DSL 36 val html = buildString {

    appendLine("<!DOCTYPE html>") appendHTML().html { lang = "ru" head { title("Опись документов") meta(charset = "utf-8", content = "text/html",) style { unsafe { raw(previewStylePart1) } } style { unsafe { raw(previewStylePart2) } } } body { renderFormContent(context.inventory) } } }
  11. Используем оба способа в зависимости от ситуации Во втором способе

    придётся повторять вёрстку в коде 1 2 3 Выводы по HTML В первом способе можно отдать реализацию на фронт 37
  12. Использование из kotlin wkhtml2pdf 43 val commands = ArrayList<String>(globalSettings.size +

    3) commands.add(wkHtmlToPdfPath) commands.addAll(globalSettings) commands.add(sourceHtml.toAbsolutePath().toString()) commands.add(destinationPdf.toAbsolutePath().toString()) return withContext(Dispatchers.IO) { val process = ProcessBuilder(commands).start() launch { pipeToLoggerBlocking("stdout", process.inputStream) } launch { pipeToLoggerBlocking("stderr", process.errorStream) } try { process.onExit().await() } catch (e: CancellationException) { process.destroyForcibly() throw e } process.exitValue() }
  13. Нативный бинарник, требующий особый docker образ 1 2 Проблемы wkhtml2pdf

    Плохая поддержка современных стандартов, например display: flex 47
  14. Пример печати из js в firefox 51 function printDocument(documentId) {

    var doc = document.getElementById(documentId); </Wait until PDF is ready to print if (typeof doc.print <<= 'undefined') { setTimeout(function(){printDocument(documentId);}, 1000); } else { doc.print(); } }
  15. Пример использования 58 val session = checkNotNull(cdpSession) { "Printer is

    closed" } try { session.print(htmlPath, destPath, settings) } catch (e: CdpException) { throw PrintingException("Cdp4j reported an error", e) } catch (e: Exception) { </ CDP4J may throw any exception (NPE for example) if Chrome is died </ Try to reinitialize it sessionMutex.withLock { cdpSession = session.recreate() } throw PrintingException("Unexpected error", e) }
  16. Печатает медленно Переодические ошибки На проде всё умерло 1 3

    2 Выводы по CDP4J 61 Сложно идентифицировать проблему, что браузер умер понимали при попытке печати 4
  17. 1. Запустить браузер 2. Соединиться с ним 3. Отправить команды

    4. Возможность открыть нужную страницу 5. Вызвать печать Что нам нужно от браузера 65
  18. ChromeCommandLine 67 class ChromeCommandLine private constructor( val prefix: List<String>, val

    command: String, private val _options: Map<ChromeSwitch, ChromeOption> ) { val options: Collection<ChromeOption> get() = _options.values operator fun get(switch: ChromeSwitch.Binary): Boolean { return _options.containsKey(switch) } operator fun get(switch: ChromeSwitch.SingleValue): String? { return _options[switch]<.value }
  19. PipeLauncher 68 <* * Chrome uses file descriptors 3 and

    4 for CDP I/O. * These descriptors are inaccessible from plain Java. * The only available are 0 (stdin), 1 (stdout), and 2 (stderr). * * Implementation of this interface launches Chrome in such a way * that resulting Process's stdin and stdout are used for CDP I/O. </ internal interface PipeLauncher { suspend fun launchChrome(commandLine: ChromeCommandLine): Process }
  20. BashPipeLauncher 69 <* * Starts Chrome via a Bash command,

    * which redirects descriptors 3 and 4 to 0 and 1 respectively. * * This approach has a downside: any garbage in Chrome's stdout will break the communication. * Some launcher scripts for Chromium in Linux distributions print debug output there. </ internal object BashExecPipeLauncher : PipeLauncher { private val specialChars = listOf(' ', '\'', '"', '<', '>', '&', '\\') override suspend fun launchChrome(commandLine: ChromeCommandLine): Process { val bashCommand = buildList { add("exec") add(commandLine.command) for (option in commandLine.options) { add(quoteArg(option.toString())) } add("3<&0") add("4>&1") }
  21. BashPipeLauncher 70 val bashCommandLine = buildList { addAll(commandLine.prefix) add("bash") add("-c")

    add(bashCommand.joinToString(" ")) } return withContext(Dispatchers.IO) { ProcessBuilder(bashCommandLine).start() } }
  22. 1. Запустить браузер 2. Соединиться с ним 3. Отправить команды

    4. Возможность открыть нужную страницу 5. Вызвать печать Что нам нужно от браузера 71
  23. CdpMessageStream 73 internal interface CdpMessageStream { fun readMessage(): ObjectNode? fun

    writeMessage(message: ObjectNode) interface Codec { val encoding: String fun createMessageStream(input: InputStream, output: OutputStream): CdpMessageStream } }
  24. NullSeperatedJsonStreamCodec 74 internal object NullSeparatedJsonStreamCodec : CdpMessageStream.Codec { private val

    jsonFactory = ObjectMapper().factory override val encoding: String get() = "JSON" override fun createMessageStream(input: InputStream, output: OutputStream): CdpMessageStream { val messageReader = StreamReader(input) val charDecoder = StandardCharsets.UTF_8.newDecoder().apply { onMalformedInput(CodingErrorAction.REPORT) onUnmappableCharacter(CodingErrorAction.REPORT) } val generator = jsonFactory.createGenerator(output).apply { disable(JsonGenerator.Feature.FLUSH_PASSED_TO_STREAM) disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET) }
  25. ChromeConnection 75 interface ChromeConnection : SuspendingCloseable { suspend fun send(message:

    ObjectNode) fun subscribe(subscriber: Subscriber) interface Subscriber { suspend fun onIncomingMessage(message: ObjectNode) fun onConnectionClosed() } }
  26. PipeChromeConnection 76 @OptIn(DelicateCoroutinesApi<:class, ExperimentalCoroutinesApi<:class) internal class PipeChromeConnection private constructor( private

    val process: Process, private val codec: CdpMessageStream.Codec ) : ChromeConnection { private val subscriber = CompletableDeferred<ChromeConnection.Subscriber>() private val outgoing = Channel<ObjectNode>() @Volatile private var closed = false private lateinit var job: Job
  27. PipeChromeConnection reader 77 launch(CoroutineName("Chrome pipe reader")) { val subscriber =

    subscriber.await() while (true) { val message = try { messageStream.readMessage() <: break } catch (e: IOException) { if (closed) { break } else { throw e } } subscriber.onIncomingMessage(message) yield() } logger.debug { "End of stream reached" } }
  28. PipeChromeConnection writer 78 launch(CoroutineName("Chrome pipe writer")) { while (true) {

    val receiveResult = outgoing.receiveCatching() receiveResult.exceptionOrNull()<.let { throw it } val message = receiveResult.getOrNull() <: break try { messageStream.writeMessage(message) } catch (e: IOException) { if (closed) { break } else { throw e } } yield() } }
  29. PipeChromeConnection sterr 79 launch(CoroutineName("Chrome stderr logger")) { val stderr =

    process.errorStream try { val reader = stderr.bufferedReader() while (true) { val line = reader.readLine() <: break stderrLogger.info { line } } logger.debug { "End of stderr" } } catch (e: IOException) { if (!closed) { logger.error(e) { "Failed to read stderr" } } } }
  30. PipeChromeLauncher 81 object PipeChromeLauncher : AbstractChromeLauncher() { private val pipeLauncher

    = BashExecPipeLauncher private val streamCodec = NullSeparatedJsonStreamCodec override suspend fun launchChrome(commandLine: ChromeCommandLine): ChromeConnection { requireNoDebuggingOptionsSet(commandLine) val commandWithDebugging = commandLine.modify { set(ChromeSwitches.remoteDebuggingPipe, streamCodec.encoding) } val process = pipeLauncher.launchChrome(commandWithDebugging) return PipeChromeConnection.open(process, streamCodec) } }
  31. 1. Запустить браузер 2. Соединиться с ним 3. Отправить команды

    4. Возможность открыть нужную страницу 5. Вызвать печать Что нам нужно от браузера 82
  32. RpcConnection 83 interface RpcConnection : SuspendingCloseable { suspend fun <R>

    useBrowserSession(block: SessionUsage<R>): R suspend fun <R> useSession(id: String, block: SessionUsage<R>): R }
  33. DefaultRpcConnection 84 class DefaultRpcConnection private constructor( private val connection: ChromeConnection

    ) : RpcConnection { private val closed = AtomicBoolean(false) private val nextRequestId = AtomicLong(1) </ Modifications must be performed with lock private val sessions = ConcurrentHashMap<String, MutableCollection<RpcSessionImpl<>() internal lateinit var browserVersion: BrowserVersion private set
  34. DefaultRpcConnection openSession 85 private fun openSession(id: String?, scope: CoroutineScope): RpcSessionImpl

    { return synchronized(sessions) { if (closed.get()) { throw ConnectionClosedException("Connection $connection is closed") } val key = getSessionKey(id) val sessionsById = sessions.computeIfAbsent(key) { CopyOnWriteArrayList() } RpcSessionImpl(id, this, nextRequestId, scope).also { it.open() sessionsById.add(it) } } }
  35. RpcSession 86 /** * Client side of CDP session. */

    interface RpcSession { val connection: RpcConnection /** * Execute an RPC request. * * @throws RpcErrorException if server signals an error * @throws ConnectionClosedException if this session is disconnected * @throws IllegalStateException if this session is closed */ suspend fun executeRequest(methodName: String, params: ObjectNode): ObjectNode fun subscribe(methodName: String, callback: (ObjectNode) <> Unit): EventSubscription interface EventSubscription : AutoCloseable }
  36. RpcSessionImpl 87 internal class RpcSessionImpl( internal val sessionId: String?, override

    val connection: DefaultRpcConnection, private val nextRequestId: AtomicLong, private val scope: CoroutineScope ) : RpcSession { private val state = AtomicReference(SessionState.ACTIVE) private val activeRequests = ConcurrentHashMap<Long, CompletableDeferred<RpcResult<>() private val subscriptions = ConcurrentHashMap<String, MutableCollection<Subscription<>() private lateinit var incomingMessages: SendChannel<IncomingMessage>
  37. HealthCheckingConnection 88 class HealthCheckingRpcConnection( private val delegate: RpcConnection, private val

    period: Duration = DEFAULT_PERIOD, private val timeout: Duration = DEFAULT_TIMEOUT ) : RpcConnection
  38. HealthCheckingConnection browser connection 89 val browserDomain = BrowserDomain(session) while (isActive)

    { delay(period) try { withDeadlineAfter(timeout) { browserDomain.getVersion() } } catch (e: Exception) { if (e is CancellationException <& e !is TimeoutCancellationException) { throw e } else { logger.error { "Browser session health check failed: $e; connection will be closed" } [email protected]() throw HealthCheckFailedException("Health check of browser session failed", e) } } }
  39. HealthCheckingConnection certain session 90 val inspectorDomain = InspectorDomain(session) launch {

    inspectorDomain.subscribeFirst(TargetCrashed).await() throw HealthCheckFailedException("Target crashed") } launch { val event = inspectorDomain.subscribeFirst(DetachedEvent).await() throw HealthCheckFailedException("Detached from target: ${event.reason}") }
  40. 1. Запустить браузер 2. Соединиться с ним 3. Отправить команды

    4. Возможность открыть нужную страницу 5. Вызвать печать Что нам нужно от браузера 91
  41. CDPDomain 94 abstract class CdpDomain<in E : CdpEvent> internal constructor(

    private val session: RpcSession ) { protected abstract val id: String private var eventsEnabled = false private var eventsEnableMutex = Mutex() protected suspend fun <T : Any> invoke( methodName: String, params: ObjectNode = EMPTY_TREE, parser: (ObjectNode) <> T ): T { val result = session.executeRequest("$id.$methodName", params) return parser(result) }
  42. PageDomain navigate 96 /** * Navigates current page to the

    given URL. * * @param url URL to navigate the page to. * @param referrer Referrer URL. * @param transitionType Intended transition type. * @param frameId Frame id to navigate, if not specified navigates the top frame. */ suspend fun navigate( url: String, referrer: String? = null, transitionType: TransitionType? = null, frameId: FrameId? = null ): NavigateResult { val params = jsonObject().apply { put("url", url) if (referrer <= null) put("referrer", referrer) if (transitionType <= null) put("transitionType", transitionType.value) if (frameId <= null) put("frameId", frameId.value) } return invoke("navigate", params) { NavigateResult.fromTree(it) } }
  43. PageDomain navigate result 97 /** * Result of invoking [PageDomain.navigate].

    * * @property frameId Frame id that has navigated (or failed to navigate) * @property loaderId Loader identifier. * @property errorText User friendly error message, present if and only if navigation has failed. */ class NavigateResult( val frameId: FrameId, val loaderId: LoaderId?, val errorText: String? ) { companion object { fun fromTree(tree: ObjectNode): NavigateResult { return NavigateResult( frameId = FrameId(tree.getString("frameId")), loaderId = tree.getStringOrNull("loaderId")<.let { LoaderId(it) }, errorText = tree.getStringOrNull("errorText") ) } } }
  44. 1. Запустить браузер 2. Соединиться с ним 3. Отправить команды

    4. Возможность открыть нужную страницу 5. Вызвать печать Что нам нужно от браузера 98
  45. PageDomain print 100 suspend fun printToPdf( landscape: Boolean? = null,

    displayHeaderFooter: Boolean? = null, printBackground: Boolean? = null, scale: Double? = null, paperWidth: Double? = null, paperHeight: Double? = null, marginTop: Double? = null, marginBottom: Double? = null, marginLeft: Double? = null, marginRight: Double? = null, pageRanges: String? = null, ignoreInvalidPageRanges: Boolean? = null, headerTemplate: String? = null, footerTemplate: String? = null, preferCSSPageSize: Boolean? = null, transferMode: PdfTransferMode? = null, ): PrintToPdfResult {
  46. PageDomain print 101 val params = jsonObject().apply { if (landscape

    <= null) put("landscape", landscape) if (displayHeaderFooter <= null) put("displayHeaderFooter", displayHeaderFooter) if (printBackground <= null) put("printBackground", printBackground) if (scale <= null) put("scale", scale) if (paperWidth <= null) put("paperWidth", paperWidth) if (paperHeight <= null) put("paperHeight", paperHeight) if (marginTop <= null) put("marginTop", marginTop) if (marginBottom <= null) put("marginBottom", marginBottom) if (marginLeft <= null) put("marginLeft", marginLeft) if (marginRight <= null) put("marginRight", marginRight) if (pageRanges <= null) put("pageRanges", pageRanges) if (ignoreInvalidPageRanges <= null) put("ignoreInvalidPageRanges", ignoreInvalidPageRanges) if (headerTemplate <= null) put("headerTemplate", headerTemplate) if (footerTemplate <= null) put("footerTemplate", footerTemplate) if (preferCSSPageSize <= null) put("preferCSSPageSize", preferCSSPageSize) if (transferMode <= null) put("transferMode", transferMode.value) } return invoke("printToPDF", params) { PrintToPdfResult.fromTree(it) } }
  47. PageDomain print result 102 class PrintToPdfResult( val data: String, @property:CdpExperimental

    val stream: StreamHandle? ) { companion object { fun fromTree(tree: ObjectNode): PrintToPdfResult { return PrintToPdfResult( data = tree.getString("data"), stream = tree.getStringOrNull("stream")<.let { StreamHandle(it) } ) } } }
  48. 1. Запустить браузер 2. Соединиться с ним 3. Отправить команды

    4. Возможность открыть нужную страницу 5. Вызвать печать Что нам нужно от браузера 103
  49. PageDomain captureScreenshot 108 suspend fun captureScreenshot( format: ImageFormat? = null,

    clip: Viewport? = null, captureBeyondViewport: Boolean? = null, ): ByteBuffer { val params = jsonObject().apply { when (format) { is ImageFormat.Jpeg <> { put("format", format.format) if (format.quality <= null) { put("quality", format.quality) } } ImageFormat.Png <> { put("format", format.format) } null <> { } }
  50. PageDomain captureScreenshot 109 if (clip <= null) { val clipJson

    = jsonObject().apply { put("x", clip.x) put("y", clip.y) put("width", clip.width) put("height", clip.height) put("scale", clip.scale) } replace("clip", clipJson) } if (captureBeyondViewport <= null) { put("captureBeyondViewport", captureBeyondViewport) } } return invoke("captureScreenshot", params) { val data = it.getString("data") val decoder = Base64.getDecoder() ByteBuffer.wrap(decoder.decode(data)) } }
  51. Привет! Не знаю кого позвать. Проблема такая: у вас код

    в кубере порождает кучу зомби-процессов “chromium”. По данным на 15.06.22 11:40 это >14k таких процессов на одной ноде. Если далее так пойдет, то ваше приложение всю ноду положит после исчерпания пула пидов. Посмотрите пжл, что можно с этим сделать. 111 Утекли pid
  52. Выводы по сервису печати Легко использовать в других командах Лёгкое

    версионирование и исправление ошибок в CDP4K 1 2 3 Изоляция хрома 117
  53. Шаблоны могут делать frontend разработчики по современным стандартам Удалось шаблонизировать

    документы 1 2 3 Итоги по html->pdf Есть переиспользуемый сервис печати 118
  54. Пример фрагмента сохраненной XML 130 <w:sectPr w:rsidR="00D44689" w:rsidRPr="00260195"> <w:pgSz w:w="11906"

    w:h="16838" <> <w:pgMar w:top="1134" w:right="850" w:bottom="1134" w:left="1701" w:header="708" w:footer="708" w:gutter="0" <> <w:cols w:space="708" <> <w:docGrid w:linePitch="360" <> </w:sectPr>
  55. Форматируем через XML 131 val cTAbstractNumDecimalXML = "<w:abstractNum xmlns:w=\"http:</schemas.openxmlformats.org/wordprocessingml/2006/main\" w:abstractNumId=\"0\">"

    + "<w:multiLevelType w:val=\"hybridMultilevel\"<>" + "<w:lvl w:ilvl=\"0\"><w:start w:val=\"1\"<><w:numFmt w:val=\"decimal\"<><w:lvlText w:val=\"%1.\"<><w:lvlJc w:val=\"left\"<><w:pPr><w:ind w:left=\"720\" w:hanging=\"360\"<></w:pPr></w:lvl "<w:lvl w:ilvl=\"1\" w:tentative=\"1\"><w:start w:val=\"1\"<><w:numFmt w:val=\"decimal\"<><w w:val=\"%1.%2\"<><w:lvlJc w:val=\"left\"<><w:pPr><w:ind w:left=\"1440\" w:hanging=\"360\"<></w:pPr></w: "<w:lvl w:ilvl=\"2\" w:tentative=\"1\"><w:start w:val=\"1\"<><w:numFmt w:val=\"decimal\"<><w w:val=\"%1.%2.%3\"<><w:lvlJc w:val=\"left\"<><w:pPr><w:ind w:left=\"2160\" w:hanging=\"360\"<></w:pPr>< + "</w:abstractNum>" fun XWPFNumbering.createNum(): BigInteger { val cTNumbering = CTNumbering.Factory.parse(cTAbstractNumDecimalXML) val cTAbstractNum = cTNumbering.getAbstractNumArray(0) val abs = XWPFAbstractNum(cTAbstractNum) return addNum(addAbstractNum(abs)) }
  56. Для сложных кейсов верстки используем вставки XML Выводы по apache

    poi Делаем DOCX с помощью apache 1 2 3 Подставляем в шаблон DOCX переменные 132 Работает под одну систему 4
  57. Пошли на встречу • Делаем доработки для B2B клиента по

    запросу • Уходило где-то 1-2 часа бекенд разработчика 136
  58. Какой XML в итоге получился 143 </w:rPr> <w:t>Меня зовут </w:t>

    </w:r><w:r> <w:t>{{</w:t></w:r> <w:r><w:rPr><w:rStyle w:val="T1"<></w:rPr> <w:t>fio</w:t> </w:r> <w:r><w:t>}}</w:t> </w:r> </w:p>
  59. Стоимость на 10 разработчиков и неограниченное число сайтов 3 726

    587 рублей 1 2 Выводы по ASPOSE Непонятно было как использовать под наш кейс, документации мало 147
  60. document.xml 152 <w:p> <w:pPr> <w:pStyle w:val="Normal"<> <w:bidi w:val="0"<> <w:jc w:val="left"<>

    <w:rPr></w:rPr> </w:pPr> <w:r> <w:rPr></w:rPr> <w:t>{{hello}}</w:t> </w:r> </w:p>
  61. Одна из частей imageTag 164 <pic:pic xmlns:pic="http:</schemas.openxmlformats.org/drawingml/2006/picture"> <pic:nvPicPr> <pic:cNvPr id="1"

    name="customImage" descr=""></pic:cNvPr> <pic:cNvPicPr> <a:picLocks noChangeAspect="1" noChangeArrowheads="1"<> </pic:cNvPicPr> </pic:nvPicPr> <pic:blipFill> <a:blip r:embed="rIdCustom%s"></a:blip> <a:stretch> <a:fillRect<> </a:stretch> </pic:blipFill> <pic:spPr bwMode="auto"> <a:xfrm> <a:off x="0" y="0"<> <a:ext cx="4000000" cy="4000000"<> </a:xfrm> <a:prstGeom prst="rect"> <a:avLst<> </a:prstGeom> </pic:spPr> </pic:pic>
  62. Сэкономили Выводы по экспорту в DOCX Можно делать индивидуальные шаблоны

    под клиентов Решение получилось простым 1 2 3 4 Шаблонизируем docx 165
  63. Apache fop хорошее решение, если у вас стоят задачи только

    трансформации xml CDP4J простой в использовании, но сложен в поиске ошибок и не отполирован до конца 1 2 3 Итоги По библиотекам ASPOSE полиглотный, но имеет скудную документацию и дорогой 166 4 Saxon неплохое решение для трансформации xml->html
  64. Выложили библиотеку в opensource Распространили подобное решение на docx 1

    2 3 Итоги Что удалось достичь Создали сервис печати используемый в нескольких командах 167
  65. Делать решение на несколько команд компании сразу Искать что есть

    в других командах внутри компании, не бояться общаться 1 2 3 Итоги Что стоит попробовать, если стоит такая же задача Puppeteer 168
  66. Имя Фамилия Должность Презентации с фото спикера на обложке Дизайн

    и юзабилити ЦВЕТ ОБЛОЖКИ отображает роль спикера или направление доклада Далее презентация универсальная для всех ролей и цвета используются для разграничения разных разделов презентации Обложку с фото можно собрать в Фигме самостоятельно ссылка , или запросить помощь дизайнера 179
  67. Презентации с фото спикера на обложке Имя Фамилия Должность тестирование

    Обложку с фото можно собрать в Фигме самостоятельно ссылка , или запросить помощь дизайнера 180
  68. 2 3 4 5 название раздела Полезная информация • Переноси

    «висячие предлоги» на новою строку, нажав Shift+Enter • Формулируй пункты кратко. Это просто якоря выступления, а не конспект • Пунктов не должно быть много. Чем короче, тем легче воспринимать информацию • Чтобы выбрать макет, щелкни стрелку рядом с полем Создать слайд, выбери нужный шаблон и наполни его информацией 6 • В Google slides используем шрифт Arial 181
  69. 00 2 3 4 5 название раздела 1 3 4

    5 название раздела 1 2 4 5 название раздела 1 2 3 5 название раздела 1 2 3 4 название раздела Для большей выразительности на слайде можно использовать Маркировку разделов презентации. Для этого нужно: скопировать ее > вставить на слайд > указать короткое название раздела в соответствующем поле. Маркировка всегда располагается в левом верхнем углу Если разделов менее 6, то удалите лишние цифры маркировки. Если разделов более 6, маркировку использовать не стоит Вначале все разделы кроме открытого белые. После того как раздел пройден, он закрашивается в один из соответствующих цветов Нумерация страниц 6 6 6 6 6 1 2 3 4 название раздела 5 182
  70. → 3 00 2 3 4 5 Там где называния

    разделов слишком длинные текущий раздел можно маркировать цифрой со стрелкой . Для этого нужно: скопировать ее > вставить на слайд > указать название раздела в соответствующем поле. Маркировка всегда располагается в левом верхнем углу Если разделов менее 6, то удалите лишние цифры маркировки. Если разделов более 6, маркировку использовать не стоит Вначале все разделы кроме открытого белые. После того как раздел пройден, он закрашивается в один из соответствующих цветов 6 1 2 3 4 5 6 1 2 3 4 5 6 1 3 4 5 6 → 2 → 1 1 2 4 5 6 → 4 1 2 3 5 6 → 5 1 2 3 4 6 → 6 1 2 3 4 5 183
  71. План доклада Пункт первый Второй пункт Третий пункт Четвертый пункт

    Пятый пункт 00 1 2 3 4 5 План доклада если 5 пунктов или меньше. Если в докладе больше разделов используйте нумерацию со следующей страницы 184
  72. План доклада Текст. Описание пункта 00 1 2 3 4

    5 6 7 8 План доклада если в докладе больше 5 разделов Текст. Описание пункта Текст. Описание пункта Текст. Описание пункта Текст. Описание пункта Текст. Описание пункта Текст. Описание пункта Текст. Описание пункта 185
  73. 186

  74. 00 Иллюстрация для перебивки разделов крупная 3D иконка 2 3

    4 5 название раздела (Для иллюстрирования перебивок можно использовать крупные 3D иконки их следует экспортировать в размере х3) Все 3D иконки в разных цветах можно найти в фигме по ссылке 6 193
  75. Заголовок схемы Текст внутри ячейки. в несколько строк. в несколько

    строк Текст внутри ячейки. несколько строк Текст внутри ячейки. в несколько строк. в несколько строк Текст внутри ячейки. в несколько строк. Акцентированная ячейка Текст несколько строк. текст несколько строк 00 194
  76. 2 3 4 5 название раздела 00 ▪ Элементы списка

    ▪ Элементы списка ▪ Элементы списка 200
  77. #1 #2 #3 2 3 4 5 название раздела 00

    Слайд с цифрами 201
  78. 202

  79. Недопустимо использовать более 4 таких иконок на слайде. Все иконки

    в разных цветах можно найти в фигме по ссылке Заголовок один Заголовок два Заголовок три ЗD иконки используются там, где необходим яркий акцент. Слайд с крупными 3D иконками 2 3 4 5 название раздела 204
  80. ЗD иконки используются там, где необходим яркий акцент. Недопустимо использовать

    более 4 таких иконок на слайде. Все иконки в разных цветах можно найти в фигме по ссылке Четвертый пункт 2 3 4 5 название раздела Слайд с крупными 3D иконками 205
  81. ЗD иконки используются там, где необходим яркий акцент. Недопустимо использовать

    более 4 таких иконок на слайде. Все иконки в разных цветах можно найти в фигме по ссылке 00 Слайд с крупными 3D иконками 1 3 4 5 название раздела 206
  82. Иконки линейные Цвет иконки можно менять. Нужно выделить иконку >

    открыть вкладку Формат рисунка > выбрать Сплошная заливка > Цвет 209
  83. 210

  84. 00 • Если нужно показать данные — воспользуйся диаграммой •

    Используй цвета из шаблона • Чтобы данные были отчетливо видны — старайся не менять размеры шрифтов Диаграмма 1 2 3 5 название раздела 6 212