diff --git a/bigbluebutton-html5/.meteor/.finished-upgraders b/bigbluebutton-html5/.meteor/.finished-upgraders index 4538749ab812db7df6cff962d3b43de5108a1ebb..c07b6ff75a66bb7295a23f06c269d8ca7f4c2bee 100644 --- a/bigbluebutton-html5/.meteor/.finished-upgraders +++ b/bigbluebutton-html5/.meteor/.finished-upgraders @@ -16,3 +16,4 @@ notices-for-facebook-graph-api-2 1.4.3-split-account-service-packages 1.5-add-dynamic-import-package 1.7-split-underscore-from-meteor-base +1.8.3-split-jquery-from-blaze diff --git a/bigbluebutton-html5/.meteor/packages b/bigbluebutton-html5/.meteor/packages index a81e24378f06a0f61bdb3b44e4aa916cc6104729..acae08d1c9c2d318a7fa9b0d9057b05207fee0ce 100644 --- a/bigbluebutton-html5/.meteor/packages +++ b/bigbluebutton-html5/.meteor/packages @@ -5,13 +5,13 @@ meteor-base@1.4.0 mobile-experience@1.0.5 -mongo@1.8.0-beta190.4 +mongo@1.8.0 reactive-var@1.0.11 -standard-minifier-css@1.6.0-beta190.4 -standard-minifier-js@2.6.0-beta190.4 +standard-minifier-css@1.6.0 +standard-minifier-js@2.6.0 es5-shim@4.8.0 -ecmascript@0.14.0-beta190.4 +ecmascript@0.14.0 shell-server@0.4.0 static-html diff --git a/bigbluebutton-html5/.meteor/release b/bigbluebutton-html5/.meteor/release index 52d4304f48df44855310d71dc894fee6459a0b6a..c6ae8ec13c5f0712c9900e0201143d94a6d1c0f3 100644 --- a/bigbluebutton-html5/.meteor/release +++ b/bigbluebutton-html5/.meteor/release @@ -1 +1 @@ -METEOR@1.9-beta.4 +METEOR@1.9 diff --git a/bigbluebutton-html5/.meteor/versions b/bigbluebutton-html5/.meteor/versions index 2cec7da9de54be38cb6337ff529186b1afa7d4a4..f069c4fed58c4806c6974659b2f249222da5713b 100644 --- a/bigbluebutton-html5/.meteor/versions +++ b/bigbluebutton-html5/.meteor/versions @@ -1,14 +1,14 @@ allow-deny@1.1.0 autoupdate@1.6.0 -babel-compiler@7.5.0-beta190.4 -babel-runtime@1.5.0-beta190.4 +babel-compiler@7.5.0 +babel-runtime@1.5.0 base64@1.0.12 binary-heap@1.0.11 blaze-tools@1.0.10 boilerplate-generator@1.6.0 caching-compiler@1.2.1 caching-html-compiler@1.1.3 -callback-hook@1.3.0-beta190.4 +callback-hook@1.3.0 cfs:micro-queue@0.0.6 cfs:power-queue@0.9.11 cfs:reactive-list@0.0.9 @@ -21,11 +21,11 @@ ddp-server@2.3.0 deps@1.0.12 diff-sequence@1.1.1 dynamic-import@0.5.1 -ecmascript@0.14.0-beta190.4 +ecmascript@0.14.0 ecmascript-runtime@0.7.0 -ecmascript-runtime-client@0.10.0-beta190.4 -ecmascript-runtime-server@0.9.0-beta190.4 -ejson@1.1.0 +ecmascript-runtime-client@0.10.0 +ecmascript-runtime-server@0.9.0 +ejson@1.1.1 es5-shim@4.8.0 fetch@0.1.1 geojson-utils@1.0.10 @@ -41,20 +41,20 @@ livedata@1.0.18 logging@1.1.20 meteor@1.9.3 meteor-base@1.4.0 -minifier-css@1.5.0-beta190.4 -minifier-js@2.6.0-beta190.4 +minifier-css@1.5.0 +minifier-js@2.6.0 minimongo@1.4.5 mobile-experience@1.0.5 mobile-status-bar@1.0.14 modern-browsers@0.1.4 -modules@0.15.0-beta190.4 -modules-runtime@0.12.0-beta190.4 -mongo@1.8.0-beta190.4 +modules@0.15.0 +modules-runtime@0.12.0 +mongo@1.8.0 mongo-decimal@0.1.1 mongo-dev-server@1.1.0 mongo-id@1.0.7 nathantreid:css-modules@4.1.0 -npm-mongo@3.3.0-beta190.4 +npm-mongo@3.3.0 ordered-dict@1.1.0 promise@0.11.2 random@1.1.0 @@ -69,8 +69,8 @@ session@1.2.0 shell-server@0.4.0 socket-stream-client@0.2.2 spacebars-compiler@1.1.3 -standard-minifier-css@1.6.0-beta190.4 -standard-minifier-js@2.6.0-beta190.4 +standard-minifier-css@1.6.0 +standard-minifier-js@2.6.0 static-html@1.2.2 stevezhu:lodash@4.17.2 templating-tools@1.1.2 @@ -78,5 +78,5 @@ tmeasday:check-npm-versions@0.3.2 tracker@1.2.0 underscore@1.0.10 url@1.2.0 -webapp@1.8.0-beta190.4 +webapp@1.8.0 webapp-hashing@1.0.9 diff --git a/bigbluebutton-html5/imports/api/annotations/addAnnotation.js b/bigbluebutton-html5/imports/api/annotations/addAnnotation.js index 11cb8020f3f65e3e091da217eef646163323c3c8..1f1a07dda4b77682535da8582e87dc9b28b16f49 100755 --- a/bigbluebutton-html5/imports/api/annotations/addAnnotation.js +++ b/bigbluebutton-html5/imports/api/annotations/addAnnotation.js @@ -2,6 +2,8 @@ import { check } from 'meteor/check'; const ANNOTATION_TYPE_TEXT = 'text'; const ANNOTATION_TYPE_PENCIL = 'pencil'; +const DEFAULT_TEXT_WIDTH = 30; +const DEFAULT_TEXT_HEIGHT = 20; // line, triangle, ellipse, rectangle function handleCommonAnnotation(meetingId, whiteboardId, userId, annotation) { @@ -39,6 +41,21 @@ function handleTextUpdate(meetingId, whiteboardId, userId, annotation) { id, status, annotationType, annotationInfo, wbId, position, } = annotation; + const { textBoxWidth, textBoxHeight } = annotationInfo; + const useDefaultSize = textBoxWidth === 0 && textBoxHeight === 0; + + if (useDefaultSize) { + annotationInfo.textBoxWidth = DEFAULT_TEXT_WIDTH; + annotationInfo.textBoxHeight = DEFAULT_TEXT_HEIGHT; + + if (100 - annotationInfo.x < DEFAULT_TEXT_WIDTH) { + annotationInfo.textBoxWidth = 100 - annotationInfo.x; + } + if (100 - annotationInfo.y < DEFAULT_TEXT_HEIGHT) { + annotationInfo.textBoxHeight = 100 - annotationInfo.y; + } + } + const selector = { meetingId, id, diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx index 5904c3223371697249bb90e9197dd92caea52a9c..dc24e69e9859fa061b20a7bedd09c106df1f51c3 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx @@ -521,7 +521,7 @@ class PresentationUploader extends Component { renderPresentationItem(item) { const { disableActions, oldCurrentId } = this.state; - const { intl } = this.props; + const { intl, allowDownloadable } = this.props; const isActualCurrent = item.id === oldCurrentId; const isUploading = !item.upload.done && item.upload.progress > 0; @@ -537,6 +537,10 @@ class PresentationUploader extends Component { [styles.tableItemAnimated]: isProcessing, }; + const itemActions = { + [styles.notDownloadable]: !allowDownloadable, + }; + const hideRemove = this.isDefault(item); const formattedDownloadableLabel = item.isDownloadable ? intl.formatMessage(intlMessages.isDownloadable) @@ -573,16 +577,19 @@ class PresentationUploader extends Component { {this.renderPresentationItemStatus(item)} </td> {hasError ? null : ( - <td className={styles.tableItemActions}> - <Button - className={isDownloadableStyle} - label={formattedDownloadableLabel} - aria-label={formattedDownloadableAriaLabel} - hideLabel - size="sm" - icon={item.isDownloadable ? 'download' : 'download-off'} - onClick={() => this.toggleDownloadable(item)} - /> + <td className={cx(styles.tableItemActions, itemActions)}> + {allowDownloadable ? ( + <Button + className={isDownloadableStyle} + label={formattedDownloadableLabel} + aria-label={formattedDownloadableAriaLabel} + hideLabel + size="sm" + icon={item.isDownloadable ? 'download' : 'download-off'} + onClick={() => this.toggleDownloadable(item)} + /> + ) : null + } <Checkbox ariaLabel={`${intl.formatMessage(intlMessages.setAsCurrentPresentation)} ${item.filename}`} checked={item.isCurrent} diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx index 000733f83f14dbbf2a156a2b088a14f006307dea..8f94ee6ad73745b355aa438885de0dcb28c0f2bb 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx @@ -11,7 +11,11 @@ const PresentationUploaderContainer = props => ( export default withTracker(() => { const PRESENTATION_CONFIG = Meteor.settings.public.presentation; const currentPresentations = Service.getPresentations(); - const { dispatchDisableDownloadable, dispatchEnableDownloadable, dispatchTogglePresentationDownloadable } = Service; + const { + dispatchDisableDownloadable, + dispatchEnableDownloadable, + dispatchTogglePresentationDownloadable, + } = Service; return { presentations: currentPresentations, @@ -19,6 +23,7 @@ export default withTracker(() => { fileSizeMin: PRESENTATION_CONFIG.uploadSizeMin, fileSizeMax: PRESENTATION_CONFIG.uploadSizeMax, fileValidMimeTypes: PRESENTATION_CONFIG.uploadValidMimeTypes, + allowDownloadable: PRESENTATION_CONFIG.allowDownloadable, handleSave: presentations => Service.persistPresentationChanges( currentPresentations, presentations, diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.scss b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.scss index 0b676868a7b08af392d0fc41b14ccdd81b994282..e1ba89b2943b97795397502046bb10be2690c0bb 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.scss @@ -79,6 +79,10 @@ } } +.notDownloadable { + min-width: 48px; +} + .tableItemIcon > i { font-size: 1.35rem; } diff --git a/bigbluebutton-html5/package-lock.json b/bigbluebutton-html5/package-lock.json index 52bb6eceebd27c111aa1c8698a4e9ac1bae96384..a6260cf6ef14fe7f4cbd5a4baa4d6f23a325c654 100644 --- a/bigbluebutton-html5/package-lock.json +++ b/bigbluebutton-html5/package-lock.json @@ -67,9 +67,9 @@ } }, "@babel/runtime": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.4.tgz", - "integrity": "sha512-r24eVUUr0QqNZa+qrImUk8fn5SPhHq+IfYvIoIMg0do3GdK9sMdiLKP3GYVVaxpPKORgm8KRKaNTEhAjgIpLMw==", + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.7.tgz", + "integrity": "sha512-uCnC2JEVAu8AKB5do1WRIsvrdJ0flYx/A/9f/6chdacnEZ7LmavjdsDXr5ksYBegxtuTPR5Va9/+13QF/kFkCA==", "requires": { "regenerator-runtime": "^0.13.2" }, @@ -484,12 +484,11 @@ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, "axios": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", - "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.1.tgz", + "integrity": "sha512-Yl+7nfreYKaLRvAvjNPkvfjnQHJM1yLBY3zhqAwcJSwR/6ETkanUgylgtIvkvz0xJ+p/vZuNw8X7Hnb7Whsbpw==", "requires": { - "follow-redirects": "1.5.10", - "is-buffer": "^2.0.2" + "follow-redirects": "1.5.10" } }, "axobject-query": { @@ -4892,9 +4891,9 @@ } }, "react-color": { - "version": "2.17.3", - "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.17.3.tgz", - "integrity": "sha512-1dtO8LqAVotPIChlmo6kLtFS1FP89ll8/OiA8EcFRDR+ntcK+0ukJgByuIQHRtzvigf26dV5HklnxDIvhON9VQ==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.18.0.tgz", + "integrity": "sha512-FyVeU1kQiSokWc8NPz22azl1ezLpJdUyTbWL0LPUpcuuYDrZ/Y1veOk9rRK5B3pMlyDGvTk4f4KJhlkIQNRjEA==", "requires": { "@icons/material": "^0.2.4", "lodash": "^4.17.11", @@ -5948,9 +5947,9 @@ "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" }, "tippy.js": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-5.1.2.tgz", - "integrity": "sha512-Qtrv2wqbRbaKMUb6bWWBQWPayvcDKNrGlvihxtsyowhT7RLGEh1STWuy6EMXC6QLkfKPB2MLnf8W2mzql9VDAw==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-5.1.3.tgz", + "integrity": "sha512-qZl6nPzXmfPTPmHXdnMc8N57BnJMvCqMg4KGgeR9Mi2k9QYHa3tA6O1UFn6w3vlHT/UIS21NrlMjixjcG2DeTw==", "requires": { "popper.js": "^1.16.0" } diff --git a/bigbluebutton-html5/package.json b/bigbluebutton-html5/package.json index 7db99f055b12f1b8121cae0907a94e91df7b54f1..33f5efbb51a73015d536e39357e959a42ea3d68d 100755 --- a/bigbluebutton-html5/package.json +++ b/bigbluebutton-html5/package.json @@ -29,11 +29,11 @@ } }, "dependencies": { - "@babel/runtime": "^7.7.4", + "@babel/runtime": "^7.7.7", "@browser-bunyan/server-stream": "^1.5.3", "@jitsi/sdp-interop": "0.1.14", "autoprefixer": "~9.3.1", - "axios": "^0.19.0", + "axios": "^0.19.1", "babel-plugin-react-remove-properties": "~0.2.5", "babel-runtime": "~6.26.0", "browser-bunyan": "^1.5.3", @@ -57,8 +57,8 @@ "re-resizable": "^4.11.0", "react": "^16.12.0", "react-autosize-textarea": "^5.0.1", - "react-color": "^2.17.3", "react-device-detect": "^1.11.14", + "react-color": "^2.18.0", "react-dom": "^16.12.0", "react-draggable": "^3.3.2", "react-dropzone": "^7.0.1", @@ -74,7 +74,7 @@ "redis": "~2.8.0", "sdp-transform": "2.7.0", "string-hash": "~1.1.3", - "tippy.js": "^5.1.1", + "tippy.js": "^5.1.3", "useragent": "^2.3.0", "winston": "^3.2.1", "yaml": "^1.7.2" diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index b4a102a77fe00b5867bd3a50ca1ffb9883fe25c8..266723178ba3cbc2404692ef16906b0daf7cf94b 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -178,6 +178,7 @@ public: echoTestNumber: '9196' relayOnlyOnReconnect: false presentation: + allowDownloadable: true defaultPresentationFile: default.pdf panZoomThrottle: 32 uploadEndpoint: "/bigbluebutton/presentation/upload" diff --git a/record-and-playback/core/scripts/utils/gen_poll_svg b/record-and-playback/core/scripts/utils/gen_poll_svg new file mode 100755 index 0000000000000000000000000000000000000000..c3a1b34c810f026694dceac5b0c541021f0d9d38 --- /dev/null +++ b/record-and-playback/core/scripts/utils/gen_poll_svg @@ -0,0 +1,243 @@ +#!/usr/bin/python3 + +# This file is part of BigBlueButton. +# +# BigBlueButton is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BigBlueButton is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with BigBlueButton. If not, see <http://www.gnu.org/licenses/>. + +# Required Ubuntu packages: python3 python3-attr python3-cairo python3-gi gir1.2-pango-1.0 + +import argparse +from attr import attrs, attrib +import cairo +import json + +import gi + +gi.require_version("Pango", "1.0") +gi.require_version("PangoCairo", "1.0") +from gi.repository import Pango, PangoCairo + + +@attrs +class Color(object): + r = attrib() + g = attrib() + b = attrib() + a = attrib(default=None) + + @classmethod + def from_int(cls, i, a=None): + r = ((i & 0xFF0000) >> 16) / 255.0 + g = ((i & 0x00FF00) >> 8) / 255.0 + b = ((i & 0x0000FF)) / 255.0 + return cls(r, g, b, a) + + def __iter__(self): + yield self.r + yield self.g + yield self.b + if self.a is not None: + yield self.a + + +FONT_FAMILY = "Arial" + +POLL_LINE_WIDTH = 2.0 +POLL_FONT_SIZE = 22 +POLL_BG = Color.from_int(0xFFFFFF) +POLL_FG = Color.from_int(0x000000) +POLL_BAR_FG = Color.from_int(0xFFFFFF) +POLL_BAR_BG = Color.from_int(0x333333) +POLL_VPADDING = 20.0 +POLL_HPADDING = 10.0 +POLL_MAX_LABEL_WIDTH = 0.5 +POLL_MAX_PERCENT_WIDTH = 0.25 + + +def draw_poll_result(output, num_responders, width, height, poll_data): + surface = cairo.SVGSurface(output, width, height) + ctx = cairo.Context(surface) + + ctx.set_line_join(cairo.LINE_JOIN_MITER) + ctx.set_line_cap(cairo.LINE_CAP_SQUARE) + + # Draw the background and poll outline + half_lw = POLL_LINE_WIDTH / 2.0 + ctx.set_line_width(POLL_LINE_WIDTH) + ctx.move_to(half_lw, half_lw) + ctx.line_to(width - half_lw, half_lw) + ctx.line_to(width - half_lw, height - half_lw) + ctx.line_to(half_lw, height - half_lw) + ctx.close_path() + ctx.set_source_rgb(*POLL_BG) + ctx.fill_preserve() + ctx.set_source_rgb(*POLL_FG) + ctx.stroke() + + font = Pango.FontDescription() + font.set_family(FONT_FAMILY) + font.set_absolute_size(int(POLL_FONT_SIZE * Pango.SCALE)) + + # Use Pango to calculate the label width space needed + pctx = PangoCairo.create_context(ctx) + layout = Pango.Layout(pctx) + layout.set_font_description(font) + + max_label_width = 0.0 + max_percent_width = 0.0 + for result in poll_data: + layout.set_text(result["key"], -1) + (label_width, _) = layout.get_pixel_size() + if label_width > max_label_width: + max_label_width = label_width + if num_responders > 0: + result["percent"] = "{}%".format( + int(float(result["num_votes"]) / float(num_responders) * 100) + ) + else: + result["percent"] = "0%" + layout.set_text(result["percent"], -1) + (percent_width, _) = layout.get_pixel_size() + if percent_width > max_percent_width: + max_percent_width = percent_width + + max_label_width = min(max_label_width, width * POLL_MAX_LABEL_WIDTH) + max_percent_width = min(max_percent_width, width * POLL_MAX_PERCENT_WIDTH) + + bar_height = (height - POLL_VPADDING) / len(poll_data) - POLL_VPADDING + bar_width = width - 4 * POLL_HPADDING - max_label_width - max_percent_width + bar_x = 2 * POLL_HPADDING + max_label_width + + max_num_votes = max(result["num_votes"] for result in poll_data) + + # All sizes are calculated, so draw the poll + for i, result in enumerate(poll_data): + bar_y = (bar_height + POLL_VPADDING) * i + POLL_VPADDING + if max_num_votes > 0: + result_ratio = float(result["num_votes"]) / float(max_num_votes) + else: + result_ratio = 0.0 + + bar_x2 = bar_x + (bar_width * result_ratio) + + # Draw the bar + ctx.set_line_width(POLL_LINE_WIDTH) + ctx.move_to(bar_x + half_lw, bar_y + half_lw) + ctx.line_to(max(bar_x + half_lw, bar_x2 - half_lw), bar_y + half_lw) + ctx.line_to( + max(bar_x + half_lw, bar_x2 - half_lw), bar_y + bar_height - half_lw + ) + ctx.line_to(bar_x + half_lw, bar_y + bar_height - half_lw) + ctx.close_path() + ctx.set_source_rgb(*POLL_BAR_BG) + ctx.fill_preserve() + ctx.stroke() + + # Draw the label and percentage + layout.set_ellipsize(Pango.EllipsizeMode.END) + ctx.set_source_rgb(*POLL_FG) + layout.set_width(int(max_label_width * Pango.SCALE)) + layout.set_text(result["key"], -1) + label_width, label_height = layout.get_pixel_size() + ctx.move_to( + bar_x - POLL_HPADDING - label_width, bar_y + (bar_height - label_height) / 2 + ) + PangoCairo.show_layout(ctx, layout) + layout.set_width(int(max_percent_width * Pango.SCALE)) + layout.set_text(result["percent"], -1) + percent_width, percent_height = layout.get_pixel_size() + ctx.move_to( + width - POLL_HPADDING - percent_width, + bar_y + (bar_height - percent_height) / 2, + ) + PangoCairo.show_layout(ctx, layout) + + # Draw the result count + layout.set_ellipsize(Pango.EllipsizeMode.NONE) + layout.set_width(-1) + layout.set_text(str(result["num_votes"]), -1) + votes_width, votes_height = layout.get_pixel_size() + if votes_width < (bar_x2 - bar_x - 2 * POLL_HPADDING): + # Votes fit in the bar + ctx.move_to( + bar_x + (bar_x2 - bar_x - votes_width) / 2, + bar_y + (bar_height - votes_height) / 2, + ) + ctx.set_source_rgb(*POLL_BAR_FG) + PangoCairo.show_layout(ctx, layout) + else: + # Votes do not fit in the bar, so put them after + ctx.move_to(bar_x2 + POLL_HPADDING, bar_y + (bar_height - votes_height) / 2) + ctx.set_source_rgb(*POLL_FG) + PangoCairo.show_layout(ctx, layout) + + +def main(): + parser = argparse.ArgumentParser( + description="Generate SVG poll image for BigBlueButton recording", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + allow_abbrev=False, + add_help=False, + ) + parser.add_argument("--help", action="help", help="show this help message and exit") + parser.add_argument( + "-i", + "--input", + metavar="POLL_JSON", + type=argparse.FileType("rb"), + help="JSON data for poll result", + required=True, + ) + parser.add_argument( + "-n", + "--num-responders", + metavar="N", + type=int, + help="number of people who responded to the poll", + required=True, + ) + parser.add_argument( + "-w", + "--width", + metavar="PT", + type=int, + help="width of SVG image to generate", + required=True, + ) + parser.add_argument( + "-h", + "--height", + metavar="PT", + type=int, + help="height of SVG image to generate", + required=True, + ) + parser.add_argument( + "-o", + "--output", + metavar="SVG_FILE", + help="output SVG filename", + default="poll.svg", + ) + args = parser.parse_args() + + poll_data = json.load(args.input) + + draw_poll_result( + args.output, args.num_responders, args.width, args.height, poll_data + ) + + +if __name__ == "__main__": + main() diff --git a/record-and-playback/presentation/scripts/publish/presentation.rb b/record-and-playback/presentation/scripts/publish/presentation.rb index 1307c94e246da70acc3b3ff322c1185b02c9f17c..ed4ca97b5d9b02bc5b09588648ec55eba34b5240 100755 --- a/record-and-playback/presentation/scripts/publish/presentation.rb +++ b/record-and-playback/presentation/scripts/publish/presentation.rb @@ -366,75 +366,23 @@ def svg_render_shape_poll(g, slide, shape) width = shape_scale_width(slide, data_points[2]) height = shape_scale_height(slide, data_points[3]) - result = JSON.load(shape[:result]) + result = shape[:result] num_responders = shape[:num_responders] presentation = slide[:presentation] - max_num_votes = result.map{ |r| r['num_votes'] }.max - dat_file = "#{$process_dir}/poll_result#{poll_id}.dat" - gpl_file = "#{$process_dir}/poll_result#{poll_id}.gpl" - pdf_file = "#{$process_dir}/poll_result#{poll_id}.pdf" + json_file = "#{$process_dir}/poll_result#{poll_id}.json" svg_file = "#{$process_dir}/presentation/#{presentation}/poll_result#{poll_id}.svg" - # Use gnuplot to generate an SVG image for the graph - File.open(dat_file, 'w') do |d| - result.each do |r| - d.puts("#{r['id']} #{r['num_votes']}") - end - end - File.open(dat_file, 'r') do |d| - BigBlueButton.logger.debug("gnuplot data:") - BigBlueButton.logger.debug(d.readlines(nil)[0]) - end - File.open(gpl_file, 'w') do |g| - g.puts('reset') - g.puts("set term pdfcairo size #{height / 72}, #{width / 72} font \"Arial,48\" noenhanced") - g.puts('set lmargin 0.5') - g.puts('set rmargin 0.5') - g.puts('unset key') - g.puts('set style data boxes') - g.puts('set style fill solid border -1') - g.puts('set boxwidth 0.9 relative') - g.puts('set yrange [0:*]') - g.puts('unset border') - g.puts('unset ytics') - xtics = result.map{ |r| "#{r['key'].gsub(/[`<|@{}^_]/, '').gsub('%', '%%').inspect} #{r['id']}" }.join(', ') - g.puts("set xtics rotate by 90 scale 0 right (#{xtics})") - if num_responders > 0 - x2tics = result.map{ |r| "\"#{(r['num_votes'].to_f / num_responders * 100).to_i}%%\" #{r['id']}" }.join(', ') - g.puts("set x2tics rotate by 90 scale 0 left (#{x2tics})") - end - g.puts('set linetype 1 linewidth 1 linecolor rgb "black"') - result.each do |r| - if r['num_votes'] == 0 or r['num_votes'].to_f / max_num_votes <= 0.5 - g.puts("set label \"#{r['num_votes']}\" at #{r['id']},#{r['num_votes']} left rotate by 90 offset 0,character 0.5 front") - else - g.puts("set label \"#{r['num_votes']}\" at #{r['id']},#{r['num_votes']} right rotate by 90 offset 0,character -0.5 textcolor rgb \"white\" front") - end - end - g.puts("set output \"#{pdf_file}\"") - g.puts("plot \"#{dat_file}\"") - end - File.open(gpl_file, 'r') do |d| - BigBlueButton.logger.debug("gnuplot script:") - BigBlueButton.logger.debug(d.readlines(nil)[0]) - end - # gnuplot svg rendering has issues, so we render to pdf... - ret = BigBlueButton.exec_ret('gnuplot', '-d', gpl_file) - raise "Failed to generate plot pdf" if ret != 0 - # then use pdftocairo to turn it into svg - ret = BigBlueButton.exec_ret('pdftocairo', '-svg', pdf_file, svg_file) - raise "Failed to convert poll to svg" if ret != 0 - - # Outer box to act as a poll result backdrop - g << doc.create_element('rect', - x: x + 2, y: y + 2, width: width - 4, height: height - 4, - fill: 'white', stroke: 'black', 'stroke-width' => 4) - # Poll image (note that the image is sideways and has to be rotated) + # Save the poll json to a temp file + IO.write(json_file, result) + # Render the poll svg + ret = BigBlueButton.exec_ret('utils/gen_poll_svg', '-i', json_file, '-w', "#{width.round}", '-h', "#{height.round}", '-n', "#{num_responders}", '-o', svg_file) + raise "Failed to generate poll svg" if ret != 0 + + # Poll image g << doc.create_element('image', 'xlink:href' => "presentation/#{presentation}/poll_result#{poll_id}.svg", - height: width, width: height, x: slide[:width], y: y, - transform: "rotate(90, #{slide[:width]}, #{y})") + width: width, height: height, x: x, y: y) end def svg_render_shape(canvas, slide, shape, image_id) diff --git a/record-and-playback/pyproject.toml b/record-and-playback/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..6ac832b25c34606964469d677c7fd8940b29d84f --- /dev/null +++ b/record-and-playback/pyproject.toml @@ -0,0 +1,3 @@ +[tool.black] +# Ubuntu 16.04 ships python 3.5; 18.04 has 3.6 +target-version = ["py35", "py36"]