Public
Edited
Mar 12, 2024
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
//
// Displays an interface for displaying selected portions (pageRectangles) within
// a pdfDocument.
//
async function rectViewer(pdfDocument, pageRectangles, options = {}) {
const {
width = (document.fullscreenElement ? screen.width : 800),
height = (document.fullscreenElement ? screen.height-110 : 700),
margin = 30,
mode = "sequential",
zoom = 3,
zoomPosition = "rectCenter", //
audios = []
} = options;
console.log("document.fullscreenElement ",document.fullscreenElement );
console.log("screen.height ",screen.height );
// Total Number of pages
const numPages = pdfDocument._pdfInfo.numPages;

const soundClips = htl.html`<section class="sound-clips">`;
const DictaphoneObj = await Dictaphone.build(soundClips );

for (let aud in audios){
console.log("psc aud", aud);
console.log("psc audios[aud]", audios[aud]);
DictaphoneObj.uploadAudio( audios[aud]);
}
var audio = null;
console.log("pageRectangles", pageRectangles);
// Insert rects without boxes in the pageRectangles variable
// so that we have an empty beginning and end rect to start and end the
// presentation
pageRectangles = [{ pageNo: 1 }, ...pageRectangles, { pageNo: numPages }];

const sleep = async (milliseconds) => {
await new Promise(resolve => {
return setTimeout(resolve, milliseconds)
});
};
// Navigation interface
const rectNo = Inputs.range([0, pageRectangles.length - 1], {
label: "Show selection",
step: 1,
value: 0
});
const PageInp = Inputs.range([1, numPages], {
label: "Page selection",
step: 1,
value: 1
});
// Zoom level
const maxZoom = Inputs.range([1, 10], {
label: "max zoom",
step: 0.1,
value: zoom
});
// Zoom position
const zoomPos = Inputs.radio(["rectCenter", "pageCenter"], {
label: "zoom position",
value: zoomPosition
});
const autoplay = Inputs.toggle({label: "Autoplay"})
// An svg for displaying the rects
const frame = htl.html`<svg width=${width} height=${height}>`;
frame.style.background = "gray";
// Drop shadow filter
frame.append(htl.svg`
<defs>
<filter id="dropShadow" x="-0.5" y="-0.5" width="200%" height="200%">
<feOffset result="offOut" in="SourceAlpha" dx="1" dy="1" />
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="5" />
<feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
</filter>
</defs>
`);
// The main d3 svg selections
const frameSel = d3.select(frame);
// Group for displaying pages
const pageGroup = frameSel.append("g").attr("class", "pageGroup");
// Group for displaying the clickable areas
const areaGroup = frameSel.append("g").attr("class", "areaGroup");
const icoGroup = frameSel.append("g").attr("class", "icoGroup");
// Group for displaying the highlighted rect selection
const rectGroup = frameSel.append("g").attr("class", "rectGroup");
const playpauseGroup = frameSel.append("g").attr("class", "playpauseGroup");
// Navigation variables
let currentPageNo = 0;
let currentPage = null;
let currentRect = null;
let pageTransf = null;
let currentRectSelected = false;
// Sets an input to a given value
function set(input, value) {
//audio = null;
input.value = value;
input.dispatchEvent(new Event("input"));
}
// Navigation callbacks
const nextCallback = () =>
set(rectNo, Math.min(pageRectangles.length - 1, rectNo.value + 1));
const prevCallback = () => set(rectNo, Math.max(0, rectNo.value - 1));
const closeCallback = () => set(rectNo, Math.max(0, rectNo.value));
//const ChangePageCallback = () => set(PageInp, currentPageNo);
// Navigation buttons
const buttons = Inputs.button([
["prev", prevCallback],
["play / next", nextCallback]
]);
// Sets an input to a given value
function setCurrentRectStatus(value) {
currentRectSelected = value
}
// Draws the clickable areas in the page
async function loadAreas(pageNo, delay = 500) {
let areas = pageRectangles.filter((d) => d.pageNo == pageNo && d.box);
//let t = d3.transition().delay(delay);
areaGroup
.selectAll("rect")
.data(areas, (d) => d.box)
.join(
(enter) =>
enter
.append("rect")
.each(function (d, i) {
const sel = d3.select(this);
const { box } = d;
const mbr = boxToMbr(box).transform(pageTransf);
sel
.attr("x", mbr.min.x)
.attr("y", mbr.min.y)
.attr("width", mbr.size().x)
.attr("height", mbr.size().y)
.attr("stroke", "red")
.attr("fill", "#00000000")
.attr("opacity", 0)
.transition()
.delay(delay)
.attr("opacity", 1);
}),
(update) => update,
(exit) => exit.remove()
);
areaGroup
.selectAll("rect")
.on("click", (ev, d) => {
let i = pageRectangles.indexOf(d);
set(rectNo, i);
mutable debug = { d, i };
});
console.log("areas", areas);

var ico_link = await FileAttachment("noun-sound-2111571.svg").url()
icoGroup
.selectAll("image")
.data(areas, (d) => d.box)
.join(
(enter) =>
enter
.append("image")
.attr("class", "sound-ico")
.attr("xlink:href", ico_link)
.each(function (d, i) {
//console.log("ico d.box", d.box);
const { box } = d;
//console.log("box", box);
const mbr = boxToMbr(box).transform(pageTransf);
const sel = d3.select(this);
sel
.attr("x", mbr.min.x + 2 )
.attr("y", mbr.min.y + 2 )
.attr("width", 16)
.attr("height", 16)
.style("display", function(d, i) {
return (typeof d.audio == "number"? 1:"none") ; })
.style("text-anchor", "middle")
.attr("opacity", 0)
.transition()
.delay(delay)
.attr("opacity", 1)
}),
(update) => update,
(exit) => exit.remove()
);
}
// Loads page pageNo
async function loadPage(pageNo, delay = 500) {
console.log("loadPage", { PageInp });
if (currentPageNo == pageNo) return 0;
const duration = 500;
currentPage = await getRectImage(pdfDocument, { pageNo }, width, height);
const [x, y, w, h] = currentPage.box;
const { width: imgWidth, height: imgHeight } = currentPage;
const mbr1 = new MBR(Vec(x, y), Vec(x + w, y + h));
const origin =
width - imgWidth > height - imgHeight
? Vec((width - imgWidth) / 2.0)
: Vec(0, (height - imgHeight) / 2);
const mbr2 = new MBR(
origin,
origin.add(Vec(currentPage.width, currentPage.height))
);
// Where the page is within the svg
pageTransf = mbrTransf(mbr1, mbr2);
const url = currentPage.toDataURL();
const enterFromBelow = !currentPageNo || pageNo > currentPageNo;
pageGroup
.selectAll("image")
.data([{ pageNo, origin, imgHeight, url }], (d) => d.pageNo)
.join(
(enter) =>
enter
.append("image")
.attr("href", (d) => d.url)
.attr("transform", (d) =>
enterFromBelow
? `translate(${d.origin.x},${d.origin.y + d.imgHeight + 10})`
: `translate(${d.origin.x},${d.origin.y - d.imgHeight - 10})`
)
.transition()
.delay(delay)
.duration(duration)
.attr("transform", (d) => `translate(${d.origin.x},${d.origin.y})`),
(update) => update,
(exit) =>
exit
.transition()
.delay(delay)
.duration(duration)
.attr("transform", (d) =>
enterFromBelow
? `translate(${d.origin.x},${d.origin.y - d.imgHeight - 10})`
: `translate(${d.origin.x},${d.origin.y + d.imgHeight + 10})`
)
.remove()
);
await loadAreas(pageNo, delay + duration);
currentPageNo = pageNo;
PageInp.value = currentPageNo;
return delay + duration;
}
// Highlights a rect selection
async function loadRect(rect) {
let data = [];
let icox, icoy = 0;
const pageDelay = 500;
let pageDuration = 0;
//console.log("loadRect", { rect });
const sameRect = ((rect == currentRect) && currentRectSelected)// It's the same rect
if (rect) {
pageDuration = await loadPage(rect.pageNo, pageDelay);
currentRect = rect;
}
if (rect && rect.box) {
const rectImage = (rect.rectImage =
rect.rectImage ||
(await getRectImage(pdfDocument, rect, width, height)));
const url = rectImage.toDataURL();
// MBR of the rectImage
const mbr1 = new MBR(Vec(0, 0), Vec(rectImage.width, rectImage.height));
// MBR of the rect within the page
const mbr2 = boxToMbr(rect.box);
// Transformation from the rectImage to its place in the page
var { a, b, c, d, e, f } = pageTransf.mult(mbrTransf(mbr1, mbr2));
const transf2 = `translate(${e},${f}) scale(${a},${d})`;
// Compute where the highlighted rect will land at
let mbr0;
const rectScale = Math.min(
maxZoom.value * a,
width - rectImage.width < height - rectImage.height
? (rectImage.width - margin * 2) / rectImage.width
: (rectImage.height - margin * 2) / rectImage.height
);
const halfDiagonal = Vec(rectImage.width, rectImage.height).scale(
rectScale / 2
);
//console.log("loadRect", { zoomPos });
if (zoomPos.value == "pageCenter") {
const center = Vec(width / 2, height / 2);
mbr0 = new MBR(center.sub(halfDiagonal), center.add(halfDiagonal));
} else {
// zoomPosition == "rectCenter"
const mbr2Page = mbr2.transform(pageTransf);
const center = mbr2Page.center();
mbr0 = new MBR(center.sub(halfDiagonal), center.add(halfDiagonal));
let dx = margin - mbr0.min.x;
if (dx > 0) mbr0 = mbr0.transform(Matrix.translate(dx, 0));
dx = width - margin - mbr0.max.x;
if (dx < 0) mbr0 = mbr0.transform(Matrix.translate(dx, 0));
let dy = margin - mbr0.min.y;
if (dy > 0) mbr0 = mbr0.transform(Matrix.translate(0, dy));
dy = height - margin - mbr0.max.y;
if (dy < 0) mbr0 = mbr0.transform(Matrix.translate(0, dy));
}
// Transformation from the rectImage to its highlight place
var { a, b, c, d, e, f } = mbrTransf(mbr1, mbr0);
const transf1 = `translate(${e},${f}) scale(${a},${d})`;
data = [{ rect, url, transf2, transf1, rectImage }];
//console.log("rect", rect);
icox = e;
icoy = f;
}
//console.log("loadRect", { sameRect });
// Update the rectGroup
rectGroup
.selectAll("image")
.data(data, (d) => d.transf2)
.join(
(enter) => {
enter
.append("image")
.attr("href", (d) => d.url)
.on("click", function (){
if ( audio ){
let playing = !audio.paused
console.log("playing", playing);
if (playing){
audio.pause();
}
}
closeCallback();
})
//.on("click", loadRect(0))
.attr("transform", (d) => d.transf2)
.attr("opacity", 0)
.transition()
.delay(pageDuration)
.attr("opacity", 1 )
.attr("filter", "url(#dropShadow)")
.transition()
.duration(1000)
.attr("transform", (d) => d.transf1);
},
(update) => {
if (!sameRect) {
update
.attr("filter", "")
.transition()
.duration(pageDelay)
.attr("opacity", 1)
.attr("transform", (d) => d.transf1);
setCurrentRectStatus(true);
} else {
update
.attr("filter", "")
.transition()
.duration(pageDelay)
.attr("opacity", 0)
.attr("transform", (d) => d.transf2);
setCurrentRectStatus(false);
playpauseGroup
.selectAll(".playpause-ico")
.attr("opacity", 0)
}
},
(exit) => {
exit
.attr("filter", "")
.transition()
.duration(pageDelay)
.attr("transform", (d) => d.transf2)
.remove();
}
);
if (rect && rect.box ) {

var ico_link = await FileAttachment("noun-sound-2111571.svg").url()
let data_rect = [rect]
console.log("data_rect", data_rect);
playpauseGroup
.selectAll(".playpause-ico")
.data(data_rect, (d) => d)
.join(
(enter) =>
enter
.append("image")
.attr("class", "playpause-ico")
.attr("xlink:href", ico_link)
.attr("x", icox + 5 )
.attr("y", icoy + 5 )
.attr("width", 20)
.attr("height", 20)
.on("click", function (){
d3.select(this)
.each(function (d, i){
console.log("play clicado 1 d", d);
console.log("audio", audio);
if (typeof( d.audio) == "number"){
if (audio){
let playing = !audio.paused
console.log("playing", playing);
console.log("sameRect", sameRect);
if (playing){
audio.pause();
}
if (!playing || !sameRect) {
console.log("Antes do play");
audio = new Audio(DictaphoneObj.getAudio(d.audio).src);
audio.play();
console.log("Depois do play");
}
//audio = null;
}
else {
console.log("DictaphoneObj.getAudio(d.audio)", DictaphoneObj.getAudio(d.audio))
audio = new Audio(DictaphoneObj.getAudio(d.audio).src);
console.log("Antes do play");
audio.play();
console.log("Depois do play");
}
audio.onended = function() {
console.log("The audio has ended");
if (autoplay.value){
nextCallback();
}
};
}
});
})
.on("load", function (){
console.log("onload");
//if (autoplay.value){
d3.select(this)
.each( async function (d, i){
console.log("autoplay", d);
if (typeof( d.audio) == "number"){
if (audio){
if (!audio.paused){
audio.pause();
}
}
console.log("Antes do play - Autoplay");
audio = new Audio(DictaphoneObj.getAudio(d.audio).src);
audio.play();
console.log("Depois do play - Autoplay");
audio.onended = function() {
console.log("The audio has ended");
if(autoplay.value){
nextCallback();
}
};
} else {
await sleep(5000);
if(autoplay.value){
nextCallback();
}
}
});
//};
})
.style("text-anchor", "middle")
.attr("opacity", 0)
.transition()
.delay(1000)
.attr("opacity",function (d, i){ return (typeof d.audio == "number"? 1:0);}),
(update) => {
if (!sameRect) {
update
.attr("opacity", 0)
.transition()
.delay(1000 + pageDuration)
//.duration(1000)
.attr("x", icox + 5 )
.attr("y", icoy + 5 )
.attr("opacity",function (d, i){ return (typeof d.audio == "number"? 1:0);});
update
.each( async function (d, i){
await sleep(1000);
if (autoplay.value){await sleep(1000);}
console.log("autoplay", d);
if (typeof( d.audio) == "number"){
if (audio){
if (!audio.paused){
audio.pause();
}
}
console.log("DictaphoneObj.getAudio(d.audio)", DictaphoneObj.getAudio(d.audio))
audio = new Audio(DictaphoneObj.getAudio(d.audio).src);
audio.play();
audio.onended = function() {
console.log("The audio has ended");
if(autoplay.value){
nextCallback();
}
};
} else {
await sleep(5000);
if(autoplay.value){
nextCallback();
}
}
//}
});
} else {
update
.transition()
.attr("x", icox + 5 )
.attr("y", icoy + 5 )
.attr("width", 20)
.attr("height", 20)
.duration(0)
}
},
(exit) => {
exit
.remove();
}
);
}
else if (rect && rect.box && autoplay.value) {
await sleep(5000);
if(autoplay.value){ //Caso tenha desmarcado durante delay
nextCallback();
}
}
}
async function rectNoCallback() {
//console.log("rectNoCallback", { currentPageNo });
await loadRect(pageRectangles[rectNo.value]);
}
async function PageInpCallback() {
//console.log("PageInpCallback", { PageInp });
await loadPage( PageInp.value);
}
rectNo.addEventListener("input", rectNoCallback);
maxZoom.addEventListener("input", rectNoCallback);
zoomPos.addEventListener("input", rectNoCallback);
PageInp.addEventListener("input", PageInpCallback);
await rectNoCallback();
return htl.html`
<div style="background-color: white">
<div class="flex" id="controles"><div class="center">${PageInp}</div><div class="center">${maxZoom}</div></div>
<div class="flex"><div class="center">${autoplay}</div><div class="center">${zoomPos}</div></div>
${frame}
<div style="display:none">
${soundClips}
</div>
<div class="flex" style="bottom: 10px;"><div class="center">${rectNo}</div><div class="center">${buttons}</div></div>
</div>
<style>
.flex {
display: flex;
width: ${width}px
}
.center {
//margin-left: ${width/2-40}px;
margin:auto;
}
.button {
margin-left: ${width/2-40}px;
// margin:auto;
}
</style>
<script>
$('.controles')[0].onmousemove = (function() {
var onmousestop = function() {
$('.box').css('background-color', 'red');
}, thread;
return function() {
$('.controles').css('background-color', 'black');
clearTimeout(thread);
thread = setTimeout(onmousestop, 1500);
};
})();
</script>
`;
}
Insert cell
getText(pdfDocument, 1)
Insert cell
mutable debug = []
Insert cell
Insert cell
rectViewer(pdfDocument, defaultRects, {w:document.webkitIsFullScreen ? width : 1152, z: fullscreen.value})
Insert cell
width

Insert cell
//
// Returns a transformation matrix for mapping Maximum Bounding Rectangle mbr1 into mbr2
//
function mbrTransf(mbr1, mbr2) {
const [p1, p2] = [mbr1.min, mbr1.max];
const [q1, q2] = [mbr2.min, mbr2.max];
const a = (q2.x - q1.x) / (p2.x - p1.x);
const d = (q2.y - q1.y) / (p2.y - p1.y);
const e = q1.x - a * p1.x;
const f = q1.y - d * p1.y;
return new Matrix(a, 0, 0, d, e, f);
}
Insert cell
//
// Converts a 4-array box in the form [x,y,width,height] to an MBR structure
//
function boxToMbr(box) {
return new MBR(Vec(box[0], box[1]), Vec(box[0] + box[2], box[1] + box[3]));
}
Insert cell
async function mapImages(Rectangles, wBar = 200, hBar = 90){
let separator = 5;
let wsize = wBar - 10;
let hsize = hBar - 10;
let ySum = 0;
let xbar = (wBar - wsize) /2
let imgs = [];
for (let pr of Rectangles) {
//if (pr.pageNo == pageNo) {
let [x, y, w, h] = pr.box.map((x) => x );
// if (pr == selectedRect) ctx.fillRect(x, y, w, h);
let h2 = Math.min((h / w) * wsize, hsize);
let w2 = Math.min((w / h) * hsize, wsize);
let img = await getRectImage(pdfDocument, pr, w2, h2);
//ctx.drawImage(img, x, ySum, wsize, h);
const url = img.toDataURL();
const translate = `translate(${xbar},${ySum})`;
imgs.push({ element : pr, url, x: xbar, y: ySum, wsize: w2, h: h2, translate });
ySum += h + separator;
//}
}
return imgs;
}
Insert cell
Insert cell
import { MBR, Vec, Matrix } from "@esperanc/2d-geometry-utils"
Insert cell
pdfjs = {
const lib = await require("https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.js");
lib.GlobalWorkerOptions.workerSrc =
"https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.worker.js";
return lib;
}
Insert cell
import {Dictaphone} from "@the-heitortomaz/web-dictaphone"
Insert cell
await FileAttachment("noun-sound-2111571.svg").url()

Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more