5

(Please ignore the empty squares.)

  1. without CSS view { height: 45em; }, I get: enter image description here (position overlap)
  2. with CSS view { height: 45em; }, I get: enter image description here (unwanted, position mismatch)

How can I have the blue <span> element positioned correctly in the second case?

<view style="height: 45em;">
  <pdf-page>                                                    <!-- position: relative -->
    <text class="textLayer">                                    <!-- position: absolute -->
      <span style="left: 417.34px; top: 37.8391px; ..."></span> <!-- position: absolute -->
    </text>
    <svg width="595px" height="842px" preserveAspectRatio="none" viewBox="0 0 595 842" xmlns="http://www.w3.org/2000/svg" version="1.1">
      <g ⋯><g ⋯><text><tspan></tspan></text></g></g>
    </svg>
  </pdf-page>
</view>

Here is the complete case in stackoverflow (see /* ← */ in the second pane after clicking on Show code snippet):

@namespace     url(http://www.w3.org/1999/xhtml);
@namespace svg url(http://www.w3.org/2000/svg);

/*pdf.css*/
:root {
  --pdf-page-outline-color: #aaa;
  --pdf-page-background-color: #fcfcfc;
}

pdf-file { display: contents; }
pdf-page {
  display: inline-block;
  outline: 1px solid var(--pdf-page-outline-color);
  background-color:  var(--pdf-page-background-color);
}

pdf-page { position: relative; }

/* text.css */
.textLayer {
  position: absolute;
  left: 0; top: 0; right: 0; bottom: 0;
  width: 100%; height: 100%;
 -overflow: hidden;
  opacity: 1;
 -line-height: 1;
}

.textLayer > span {
  color: transparent;
  position: absolute;
  white-space: pre;
  cursor: text;
  -webkit-transform-origin: 0% 0%;
          transform-origin: 0% 0%;
}

/**/
 view      { background: green; }
.textLayer { background: rgba(0, 255, 0, .1); }
 svg|svg   { background: rgba(255, 0, 0, .1); }
<style>
  view {
    height: 45em; /* ← */
    display: flex;
    overflow: auto;
    flex-direction: column;
    place-items: center;
    scroll-snap-type: y mandatory;
    overflow: auto;
  }

  pdf-page { height: 100%; scroll-snap-align: start; }
  svg { height: 100%; width: auto; }

  text { overflow: visible; background: rgb(0, 0, 0, .1); }
  text > span { background: rgba(0,0,255,.1); }
</style>

<view -onclick="this.requestFullscreen()">
  <pdf-page of="f" no="+1" svg="">
    <text class="textLayer">
      <span style="left: 417.34px; top: 37.8391px; font-size: 12px; font-family: sans-serif; transform: scaleX(1.07482);">Plenarprotokoll 16/3</span>
    </text>
    <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="595px" height="842px" preserveAspectRatio="none" viewBox="0 0 595 842">
      <g transform="matrix(1 0 0 -1 -8 850)">
        <g transform="">
          <text transform="matrix(12 0 0 12 425.34 801.2976) scale(1, -1)" xml:space="preserve">
            <tspan x="0 0.6672 0.9454 1.5016 2.1128 2.669 3.0582 3.6694 4.0586 4.6698 5.003 5.6142 6.1704 6.7816 7.0598 7.6132 8.1694 8.7256 9.0038" y="0" font-family="g_d0_f1" font-size="1px" fill="rgb(0,0,0)"></tspan>
          </text>
        </g>
      </g>
    </svg>
  </pdf-page>
</view>

(also available for review on codepen: https://codepen.io/cetinsert/pen/MWeVxLe?editors=1100)

Cetin Sert
  • 4,497
  • 5
  • 38
  • 76
  • Can you explain a bit more about what you're trying to do? You're trying to make the grey box overlap the blue boxes? – Eliezer Berlin Nov 05 '20 at 23:53
  • Edited the question for clarity. I want black - blue box overlap to be preserved exactly the same way in the second case as in the first one. – Cetin Sert Nov 06 '20 at 03:02

3 Answers3

3

A much more precise way is to just transform: scale(x, y) the <text> layer once on resize without any <span style> position value recalculations / unit change.


This answer has triggered the launch of my commercial project.

WebPDF.prohttps://WebPDF.pro

Zero-dependency, truly HTML-native PDF web components.


const t = document.querySelector('text');
const r = new ResizeObserver(textResize(t));
      r.observe(t);
const textResize  = t => ([ a ]) => {
  const         e = t.parentNode.lastElementChild; // <svg> | <canvas>
  const         i = PDFPageElement.image(e);       // { height, width };
  const     h = e.clientHeight;
  const x = h / i.      height;
  const     w = e.clientWidth;
  const y = w / i.      width;
                    t.style.setProperty('transform', `scale(${x}, ${y})`);
};
PDFPageElement.image = i => { if (!i) return;
  switch (i.tagName) {
    case 'CANVAS':   return { height: i.height,               width: i.width               };
    default: /*SVG*/ return { height: i.height.baseVal.value, width: i.width.baseVal.value };
  }    
};

with 1 additional CSS rule

.textLayer { overflow: visible; }

Before / After

before after

Cetin Sert
  • 4,497
  • 5
  • 38
  • 76
0

Given viewport width and height, a one-time conversion from <span style> pixels to percents:

const px2pc = ({ width, height }) => s => {
  const      c = s.style;
  const l = +c.getPropertyValue('left'     ).slice(0, -2); // drop px
  const t = +c.getPropertyValue('top'      ).slice(0, -2);
  const f = +c.getPropertyValue('font-size').slice(0, -2);
             c.setProperty     ('left',      `${(l / width)  * 100}%`);
             c.setProperty     ('top',       `${(t / height) * 100}%`);
             c.setProperty     ('font-size', `${(f / height) * 100}%`);
};

and accounting for font size adaptation in <text> element whenever its ancestors cause resize:

const t = document.querySelector('text');
const r = new ResizeObserver(textFontResize(t));
      r.observe(t);
const textFontResize = t => ([ a ]) => {
  const i = t.parentNode.lastElementChild; // <svg> | <canvas>
            t.style.setProperty('font-size', `${i.clientHeight}px`);
};

proved itself a very robust and relatively simple solution.

(If anyone comes up with a more elegant way, say without ever resorting to ResizeObserver, please post a new answer.)


Demo

(External assets are version-fixed for this question.)

  1. Scroll to the end of this answer
  2. Hit ▶️ Run code snippet
  3. Hit ⤤ Full page

<!doctype html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="//cdnjs.cloudflare.com/ajax/libs/pdf.js/2.6.347/pdf.min.js" integrity="sha512-Z8CqofpIcnJN80feS2uccz+pXWgZzeKxDsDNMD/dJ6997/LSRY+W4NmEt9acwR+Gt9OHN0kkI1CTianCwoqcjQ==" crossorigin="anonymous"></script>
    <script src="//shin.glitch.me/shin.q1.js"></script>
    <script src="//shin.glitch.me/pdf.q1.js"></script>
    
    <!-- private resources -->
    <link  href="//cdn.blue/{fa-5.15}/css/all.css" rel="stylesheet">
    <link  href="//cdn.blue/{fa+}/var.css"         rel="stylesheet">
    <link  href="//cdn.blue/{fa+}/x.css"           rel="stylesheet">
    <!-- private resources -->
    
    <style>:root { max-width: 50em; margin: auto; }</style>
    
    <script>console.clear();</script>
    <style>html, body { padding: 0; margin: 0; font-family: system-ui; }</style>
    
    <script>
class CodeEditElement extends ShinElement {
  constructor() {
    super(`
<style>:host { display: block; overflow: hidden; } pre { height: 100%; margin: 0; }</style>
<pre contenteditable spellcheck=false inputmode=text></pre>`, { _: { QS: { T: [ 'pre' ] } } });
    const e = this;
          e.ph = v => { const e = v.target; if (!e.childElementCount) return; e.textContent = e.textContent; };
  }
     connectedCallback() { this._.pre.   addEventListener('input', this.ph); }
  disconnectedCallback() { this._.pre.removeEventListener('input', this.ph); }
  get textContent()  { return this._.pre.textContent;     }
  set textContent(v) {        this._.pre.textContent = v; }
}
CodeEditElement.define();
      
class CodeLiveElement extends ShinElement {
      constructor()  { super(`<live></live>`, { _: { QS: { T: [ 'live' ] } } }); }
  get textContent()  { return this._.live.textContent; }
  set textContent(v) {        this._.live.textContent = v; }
  get innerHTML()    { return this._.live.innerHTML; }
  set innerHTML(v)   {        this._.live.innerHTML = v; this.evalScripts(); }
      evalScripts()  { this._.QA('script').forEach(s => eval(s.textContent)); }
}
CodeLiveElement.define();
            
class CodePlayElement extends ShinElement {
  constructor() {
    super(`
      <style>
        :host(:not([open])) > code-edit { display: none; }
        :host > div       { display: flex; justify-content: stretch; align-items: stretch; }
        ::slotted(select) { flex: 1; }
        * { border-color: var(--bd); }
      </style>
      <div part=controls>
        <slot></slot>
        <button id=reset><slot name=reset></slot></button>
        <button id=open><slot name=open></slot></button>
      </div>
      <code-edit id=pre part=edit></code-edit>`, 
      { _: { QS: { S: { '#pre': 'pre', '#reset': 'reset', '#open': 'open' } } } }
    );
    const e = this;
          e.sc = v => { const tx = e.tx;             e.px = tx; };
          e.pi = v => {                   e.t.ux = e.px;      };
          e.rc = v => {                              e.tr();             };
          e.oc = v => {                              e.open =!e.open;    };
    Shin.IPA(e, 'open', { t: Shin.Boolean });
  }
     connectedCallback() { setTimeout(() => this._init()); }
  disconnectedCallback() {                  this._dest();  }
  static cleanCode(t = "") {
    return t.trim().replace(/^[\n]+/g, '').replace(/\s+$/g, '').split('\n').map(l => l.trimEnd()).join('\n');
  }
  get s()   { return    this.QS('select'); }
  get S()   { const o = this.QA('option'); return o.filter(o => o.selected); }
  get t()   { return [].concat(...this.S.map(o => Shin.QA('template', o))); } 
  get tx()  { return    this.t.map(t => t.ux || this.constructor.cleanCode(t.innerHTML)).join('\n'); }
      tr()  {           this.t.ux = undefined; this.sc(); }
  get r()   { return    this._.reset; }
  get o()   { return    this._.open; }
  get p()   { return    this._.pre; }
  get P()   { return    this._.QA('pre'); }
  get px()  { return    this.p.textContent; }
  set px(v) {           this.p.textContent = v; this.oninput(); }
  _init() { 
    const e = this;                      e.sc();
          e.s.addEventListener('change', e.sc);
          e.p.addEventListener('input',  e.pi);
          e.r.addEventListener('click',  e.rc);
          e.o.addEventListener('click',  e.oc);
  }
  _dest() { 
    const e = this;
          e.s.removeEventListener('change', e.sc);
          e.p.removeEventListener('input',  e.pi);
          e.p.removeEventListener('click',  e.rc);
          e.p.removeEventListener('click',  e.oc); 
  }
}
CodePlayElement.define();
    </script>
    <style>
      body { padding: 1em; overflow: scroll; font-family: system-ui; }
      :root {
        --list-bg: #eee;        
        --code-bg: #fefefe;
        --live-bg_: #ccc;
        --bd: #ccc;
      }
      code-play    { display: flex; width: 100%; flex-direction: row-reverse; }
      code-play:not(:first-of-type) { margin-top: 1em; }
      ::part(edit)  { min-height: 1em; min-width: 1em; overflow-x: auto; background-color: var(--code-bg); }

      x[undo]:before, x[undo]:after { content: var(--fa-undo); }
      x[open]:before, x[open]:after { content: var(--fa-eye-slash); }
       [open]         x[open]:before,
       [open]         x[open]:after { content: var(--fa-eye); }

      select { background: var(--list-bg); border-color: var(--bd); overflow: auto; }
      live   { background: var(--live-bg); display: block; bordxer: 1px solid var(--bd); }
      
      code-play:not([open]) + live { _display: none; }
      ::part(edit) { border: 1px solid var(--bd); flex: 1; }
      ::part(controls) { flex-direction: column-reverse; }
      ::part() { border-radius: 3px; }
    </style>
    <style>
      code-play:not([open]) { height: 2.7em; _outline: 1px solid; }
      code-play:not([open]) > select { display: none; }
    </style>
  </head>
  <body>

    <code-live id="cl"></code-live><script>cl.innerHTML = "";</script>
    
    <script>
      const pes = 'previousElementSibling';
      const n =  (p, N = 1) => e => { let j = e[p]; for (let i = 1; i < N; i++) j = j[p]; return j; };
      const c = n(pes, 2);
      const l = n(pes, 1);
      const _ = () => document.currentScript;
    </script>
            
    <code-play open>
      <select multiple size="1">
        <option selected>file<template>
<pdf-file id="f" src="//pdf.systems/16003.pdf"></pdf-file>
<pdf-file id="g" src="//pdf.systems/16004.pdf"></pdf-file>
        </template></option>
      </select>
      <x open slot="open"></x>
      <x undo slot="reset"></x>
    </code-play>
    <live></live>
    <script>{
      const _ = document.currentScript;
          c(_).oninput = () => 
          l(_).innerHTML = c(_).px;
    }
    </script>
    
    <code-play open style="min-height: 11em;">
      
      <select multiple size="6">

        <optgroup label="File Reference">

          <option>by attribute<!-- !!!!!!!!! --><template>
<pdf-page of="f" no="+1" scale=".1"></pdf-page>
<pdf-page of="f" no="+1" scale=".2"></pdf-page>
<pdf-page of="f" no="+1" scale=".3"></pdf-page>
<pdf-page of="f" no="+1" scale=".4"></pdf-page>
<pdf-page of="f" no="+1" scale=".5"></pdf-page>
<pdf-page of="f" no="+1" scale=".5" svg=""></pdf-page>
          </template></option> 

          <option>by ancestry<!-- !!!!!!!!! --><template>
<pdf-file src="//pdf.systems/16008.pdf">
  <pdf-page no="+1" scale=".4" svg></pdf-page>
  <pdf-page no="+3" scale=".4" svg></pdf-page>
  <pdf-page no="-1" scale=".4" svg></pdf-page>
</pdf-file>
          </template></option>

        </optgroup>
        
        <optgroup label="Embed Mode">
        
          <option selected>Sized Container ⭤<!-- !!!!!!!!! --><template>
<style>
  view { width: 10em; height: 25em; /* ← */
    display: block; background: white; overflow: auto;
  }

  pdf-page        { width: 100%; }
    ::part(layer) { width: 100%; height: auto; }
</style>

<view onclick="this.requestFullscreen()">

  <pdf-page of="f" no="+1" xvg="" scale=".2"></pdf-page>
  <pdf-page of="f" no="+1" xvg="" scale="1"></pdf-page>
  <pdf-page of="f" no="+1" xvg="" scale="2"></pdf-page>
  <pdf-page of="f" no="+1" svg=""></pdf-page>
  
  <pdf-page of="f" no="-1" xvg=""></pdf-page>
  <pdf-page of="g" no="-1" svg=""></pdf-page>

</view>
            </template></option>
          
        </optgroup>
        
      </select>
      
      <x open slot="open"></x>
      <x undo slot="reset"></x>
      
    </code-play>
    <live></live>
    <script>{ const _ = document.currentScript; c(_).oninput = () => l(_).innerHTML = c(_).px; }</script>

    <style>live { display: flex; align-items: flex-end; flex-wrap: wrap; } pdf-file { display: contents; }</style>    

    <h3>Styling</h3>
    <p>Styles can be easily applied. (Try <strong><kbd>Ctrl</kbd></strong> + <i class="fa fa-mouse-pointer"></i> to unselect / select multiple.)</p>
    <code-play open>
      <select multiple size="8">
        <optgroup label="Page">
          <option>outline   <template><style>pdf-page { outline: 1px dotted; }</style></template></option>
          <option>background<template><style>pdf-page { background-color: rgb(200, 200, 255, .1); }</style></template></option>
        </optgroup>
        <optgroup label="Text">
          <option selected>mark<template><style>::part(span) { background-color: rgb(255, 0, 0, .1); }</style></template></option>
        </optgroup>
        <optgroup label="Image">
          <option>hidden     <template><style>::part(image) { opacity: 0; }</style></template></option>
          <option>pixelated  <template><style>::part(image) { image-rendering: pixelated; }</style></template></option>
          <option>crisp-edges<template><style>::part(image) { image-rendering: crisp-edges; }</style></template></option>
        </optgroup>
      </select>
      <x open slot="open"></x>
      <x undo slot="reset"></x>
    </code-play>
    <live></live>
    <script>{ const _ = document.currentScript; c(_).oninput = () => l(_).innerHTML = c(_).px; }</script>
    
    <script>
     document.addEventListener(  'load', e => console.warn('l', e.target));
     document.addEventListener('unload', e => console.warn('u', e.target));
    </script>
    <p style="margin-bottom: 10em;"><a href="https://shin.glitch.me/pa.html">Documentation (WIP)</a></p>
            
  </body>
</html>
Cetin Sert
  • 4,497
  • 5
  • 38
  • 76
  • If you're asking, `"Can CSS read current values, and transform them into other values?"`, then no, CSS can't do that, you'll need JavaScript for it. (Which you've already done.). And therefore if you want to use pure CSS, you'll need to come up with your own numbers by hand, since you're trying to put an block on top of an "arbitrary" point on an image. – Eliezer Berlin Nov 08 '20 at 07:55
  • @EliezerBerlin - After having accepted the use of `ResizeObserver`, I have come up with a much simpler answer https://stackoverflow.com/a/64740559/112882 that does not touch the `` elements at all. – Cetin Sert Nov 08 '20 at 16:59
-1

It's not really possible to do via CSS in a clean way. This, for example, will work, but because you're positioning a span on top of a picture, all of the numbers are hardcoded:

.textLayer > span{
    right: 10% !important;
    left: auto !important;
    top: 0 !important;
    margin-top: 6%;/*margin-top uses 6% of the WIDTH, not 6% of the height. It's very useful when trying to place something on top of an image.*/
    width: 20%;
    height: 2%;
}

Here's a reproduction of your snippet with the CSS added in:

@namespace     url(http://www.w3.org/1999/xhtml);
@namespace svg url(http://www.w3.org/2000/svg);

/*pdf.css*/
:root {
  --pdf-page-outline-color: #aaa;
  --pdf-page-background-color: #fcfcfc;
}

pdf-file { display: contents; }
pdf-page {
  display: inline-block;
  outline: 1px solid var(--pdf-page-outline-color);
  background-color:  var(--pdf-page-background-color);
}

pdf-page { position: relative; }

/* text.css */
.textLayer {
  position: absolute;
  left: 0; top: 0; right: 0; bottom: 0;
  width: 100%; height: 100%;
 -overflow: hidden;
  opacity: 1;
  line-height: 1;
}

.textLayer > span {
  color: transparent;
  position: absolute;
  white-space: pre;
  cursor: text;
  -webkit-transform-origin: 0% 0%;
          transform-origin: 0% 0%;
}
.textLayer > span{
    right: 10% !important;
    left: auto !important;
    top: 0 !important;
    margin-top: 6%;/*margin-top uses 6% of the WIDTH, not the height. It's sometimes more useful than ordinary top:6%.*/
    width: 20%;
    height: 2%;
}
/**/
 view      { background: green; }
.textLayer { background: rgba(0, 255, 0, .1); }
 svg|svg   { background: rgba(255, 0, 0, .1); }
<style>
  view {
    height: 45em; /* ← */
    display: flex;
    overflow: auto;
    flex-direction: column;
    place-items: center;
    scroll-snap-type: y mandatory;
    overflow: auto;
  }

  pdf-page { height: 100%; scroll-snap-align: start; }
  svg { height: 100%; width: auto; }

  text { overflow: visible; background: rgb(0, 0, 0, .1); }
  text > span { background: rgba(0,0,255,.1); }
</style>

<view -onclick="this.requestFullscreen()">
  <pdf-page of="f" no="+1" svg="">
    <text class="textLayer">
      <span style="left: 417.34px; top: 37.8391px; font-size: 12px; font-family: sans-serif; transform: scaleX(1.07482);">Plenarprotokoll 16/3</span>
    </text>
    <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="595px" height="842px" preserveAspectRatio="none" viewBox="0 0 595 842">
      <g transform="matrix(1 0 0 -1 -8 850)">
        <g transform="">
          <text transform="matrix(12 0 0 12 425.34 801.2976) scale(1, -1)" xml:space="preserve">
            <tspan x="0 0.6672 0.9454 1.5016 2.1128 2.669 3.0582 3.6694 4.0586 4.6698 5.003 5.6142 6.1704 6.7816 7.0598 7.6132 8.1694 8.7256 9.0038" y="0" font-family="g_d0_f1" font-size="1px" fill="rgb(0,0,0)"></tspan>
          </text>
        </g>
      </g>
    </svg>
  </pdf-page>
</view>

Edit: A bit more clarification.

We want to put the box on top of the text. The numbers used for the position and width/height of the text may seem arbitrary, but that's simply because the location of the item we're trying to cover ALSO has an arbitrary location/width/height. (If you like, we can talk about how to use GIMP to check the aspect ratio of your image, but..

a. I don't think using GIMP to measure the correct values is within the scope of this answer (You can math it out by taking the width of the image and height of the image to find the aspect ratio, and then use that aspect ratio along with the X/Y coordinates of the start point and the X/Y coordinates of the end point to figure out what percentages you need to use.... but, well....)

b. It's usually significantly faster to just fiddle with it in Chrome's Dev Tools for 15 minutes,

As a general rule, when using position: absolute to put something on top of an image, your code is going to look something like this:

.item{
    position:absolute;
    top:0;
    margin-top:W%; //The reason we use margin instead of top is because margin is based off width, which allows us to maintain aspect ratio on our positioning.
    left:X%; // Or right
    width:Y%;
    height:Z%;
}

Edit 2: I'd originally used vw and vh, which are often extremely useful for this kind of positioning, but in the end it was possible to refactor them out, which is why the only non-standard positioning we're using is margin-top.

Eliezer Berlin
  • 3,170
  • 1
  • 14
  • 27
  • I mean ... you mention units other than `%` and `px` but still use `%`. Also how do you calculate the values for those arbitrarily chosen CSS properties (`right` vs `left`, `margin-top`, ...)? – Cetin Sert Nov 06 '20 at 10:12