Skip to content

Commit 594341c

Browse files
committed
V:: Support for big images (up to 20K * 20K) in single page mode.
Dynamic letter spacing in text layer to make text width fit its zone. Concomitant refactoring.
1 parent d95e757 commit 594341c

File tree

3 files changed

+146
-65
lines changed

3 files changed

+146
-65
lines changed

viewer/src/components/ImageBlock/CanvasImage.jsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import Constants from '../../constants';
55

66
/**
77
* A component containing logic of rendering ImageData on canvas element.
8-
* Scales itself via css and via logarithmic scale method.
8+
* Scales itself via css and via logarithmic scale method.
9+
*
10+
* Must be used with a unique key for each page.
911
*/
1012
export default class CanvasImage extends React.Component {
1113

@@ -20,13 +22,11 @@ export default class CanvasImage extends React.Component {
2022
this.tmpCanvas = document.createElement('canvas');
2123
this.tmpCanvasCtx = this.tmpCanvas.getContext('2d');
2224
this.lastUserScale = null;
25+
this.redrawImageTimeout = -1;
2326
}
2427

25-
UNSAFE_componentWillReceiveProps(nextProps) {
26-
if (this.props.imageData !== nextProps.imageData) {
27-
this.lastUserScale = null;
28-
clearTimeout(this.redrawImageTimeout);
29-
}
28+
componentWillUnmount() {
29+
clearTimeout(this.redrawImageTimeout);
3030
}
3131

3232
componentDidUpdate() {
@@ -50,27 +50,25 @@ export default class CanvasImage extends React.Component {
5050
}
5151

5252
updateImageIfRequired() {
53-
if (!(this.canvas && this.props.imageData)) {
53+
if (!this.canvas) {
5454
return;
5555
}
56-
if (this.props.imageData && this.lastUserScale !== this.props.userScale) {
56+
if (this.lastUserScale !== this.props.userScale) {
5757
if (this.lastUserScale === null) { // if there is no image at all
5858
this.drawImageOnCanvas();
5959
}
6060
clearTimeout(this.redrawImageTimeout);
6161
this.redrawImageTimeout = setTimeout(() => {
6262
this.drawImageOnCanvas();
63-
}, 200);
63+
}, 300);
6464
}
6565
}
6666

6767
logarithmicScale() {
68-
const { imageData, imageDpi, userScale } = this.props;
68+
const image = this.props.imageData;
6969
var tmpH, tmpW, tmpH2, tmpW2;
7070

71-
var image = imageData;
72-
var scale = imageDpi ? imageDpi / Constants.DEFAULT_DPI : 1;
73-
scale /= userScale; // current scale factor compared with the initial size of the image
71+
let scale = this.getScaleFactor();
7472

7573
if (scale <= 1) {
7674
return image; // when it's scaled up, it will be just scaled with css

viewer/src/components/ImageBlock/ComplexImage.jsx

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import TextLayer from './TextLayer';
66
import Constants from '../../constants';
77
import styled from 'styled-components';
88
import LoadingPhrase from '../misc/LoadingPhrase';
9+
import memoize from "memoize-one";
910

1011
const Root = styled.div`
1112
position: relative;
@@ -47,11 +48,82 @@ class ComplexImage extends React.PureComponent {
4748
textZones: PropTypes.array,
4849
rotation: PropTypes.oneOf([0, 90, 180, 270]),
4950
outerRef: PropTypes.func,
51+
currentPageNumber: PropTypes.number,
5052
};
5153

54+
/**
55+
* Firefox cannot putImageData() bigger than about 12_000 * 12_000 pixels.
56+
* createImageBitmap() fails too.
57+
* Chrome fails on 25_000 * 22_000.
58+
* So the solution is just to scale down an image once, as if it were of a smaller size.
59+
* The current solution should be replaced with createImageBitmap() scaling
60+
* once it's supported by Firefox and Safari. Now images up to 20K * 20K pixels are supported:
61+
* the image is downscaled to half being divided into 4 quarters, using 4 canvases.
62+
* Theoretically, the image can be split into more than 4 equal blocks, so that even bigger ones are processed.
63+
* But there is a doubt the library will be able to decode bigger images,
64+
* since it sometimes fails with an out of memory error on a 16K * 12K image.
65+
*/
66+
resizeImageIfRequired = memoize((image) => {
67+
const dimensionThreshold = 10000;
68+
const { width, height } = image;
69+
const maxDimension = Math.max(width, height);
70+
71+
if (maxDimension <= dimensionThreshold) return image;
72+
73+
const createTempCanvas = (image, x, y, width, height) => {
74+
const canvas = document.createElement('canvas');
75+
canvas.width = width;
76+
canvas.height = height;
77+
const ctx = canvas.getContext('2d', { alpha: false });
78+
ctx.putImageData(image, -x, -y, x, y, width, height);
79+
return canvas;
80+
};
81+
82+
const outputCanvas = document.createElement('canvas');
83+
const outputCtx = outputCanvas.getContext('2d', { alpha: false });
84+
85+
const halfWidth = Math.floor(width / 2);
86+
const halfHeight = Math.floor(height / 2);
87+
88+
const width1 = Math.floor(halfWidth / 2);
89+
const width2 = Math.floor((width - halfWidth) / 2);
90+
const height1 = Math.floor(halfHeight / 2);
91+
const height2 = Math.floor((height - halfHeight) / 2);
92+
93+
outputCanvas.width = width1 + width2;
94+
outputCanvas.height = height1 + height2;
95+
96+
const drawImage = (x, y, width, height, destX, destY, destWidth, destHeight) => outputCtx.drawImage(
97+
createTempCanvas(image, x, y, width, height),
98+
0, 0, width, height,
99+
destX, destY, destWidth, destHeight,
100+
);
101+
102+
drawImage(
103+
0, 0, halfWidth, halfHeight,
104+
0, 0, width1, height1,
105+
);
106+
drawImage(
107+
halfWidth, 0, width - halfWidth, halfHeight,
108+
width1, 0, width2, height1,
109+
);
110+
drawImage(
111+
0, halfHeight, halfWidth, height - halfHeight,
112+
0, height1, width1, height2,
113+
);
114+
drawImage(
115+
halfWidth, halfHeight, width - halfWidth, height - halfHeight,
116+
width1, height1, width2, height2,
117+
);
118+
119+
return outputCtx.getImageData(0, 0, outputCanvas.width, outputCanvas.height);
120+
});
121+
52122
render() {
53-
const initialWidth = this.props.imageWidth || this.props.imageData.width;
54-
const initialHeight = this.props.imageHeight || this.props.imageData.height;
123+
const imageData = this.props.imageData && this.resizeImageIfRequired(this.props.imageData);
124+
125+
const initialWidth = this.props.imageWidth || imageData.width;
126+
const initialHeight = this.props.imageHeight || imageData.height;
55127

56128
const scaleFactor = Constants.DEFAULT_DPI / this.props.imageDpi * this.props.userScale;
57129

@@ -73,8 +145,13 @@ class ComplexImage extends React.PureComponent {
73145
ref={this.props.outerRef}
74146
>
75147
<div>
76-
{this.props.imageData ?
77-
<CanvasImage {...this.props} /> :
148+
{imageData ?
149+
<CanvasImage
150+
imageData={imageData}
151+
imageDpi={this.props.imageDpi}
152+
userScale={this.props.userScale}
153+
key={this.props.currentPageNumber}
154+
/> :
78155
this.props.imageUrl ?
79156
<img
80157
src={this.props.imageUrl}
Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import React from 'react';
2-
import PropTypes from 'prop-types';
1+
import React, { useEffect, useRef } from 'react';
32
import Constants from '../../constants';
43
import styled from 'styled-components';
54

@@ -19,65 +18,72 @@ const Root = styled.div`
1918
const TextZone = styled.div`
2019
line-height: initial;
2120
color: rgba(0, 0, 0, 0);
22-
white-space: nowrap;
2321
text-align-last: justify;
2422
text-align: justify;
2523
position: absolute;
2624
box-sizing: border-box;
27-
font-family: 'Times New Roman', Times, serif;
25+
font-family: 'Times New Roman', Garamond, Times, serif;
26+
27+
span {
28+
white-space: pre;
29+
}
2830
`;
2931

30-
class TextLayer extends React.Component {
32+
const TextLayer = ({ textZones, imageHeight, imageWidth, userScale, imageDpi }) => {
33+
const wrapper = useRef(null);
3134

32-
static propTypes = {
33-
zone: PropTypes.object,
34-
imageHeight: PropTypes.number,
35-
imageWidth: PropTypes.number
36-
};
35+
useEffect(() => {
36+
if (!wrapper.current) return;
3737

38-
render() {
39-
const { textZones, imageHeight, imageWidth } = this.props;
40-
if (!textZones) {
41-
return null;
38+
for (const textZone of wrapper.current.children) {
39+
const span = textZone.firstChild;
40+
if (span.offsetWidth < textZone.offsetWidth) {
41+
const letterSpacing = (textZone.offsetWidth - span.offsetWidth) / span.innerText.length;
42+
span.style.letterSpacing = letterSpacing + 'px';
43+
}
4244
}
43-
const scaleFactor = Constants.DEFAULT_DPI / this.props.imageDpi * this.props.userScale;
44-
const scaledWidth = Math.floor(imageWidth * scaleFactor);
45-
const scaledHeight = Math.floor(imageHeight * scaleFactor);
45+
}, [textZones, wrapper.current]);
46+
47+
if (!textZones) return null;
4648

47-
return (
48-
<Root
49+
const scaleFactor = Constants.DEFAULT_DPI / imageDpi * userScale;
50+
const scaledWidth = Math.floor(imageWidth * scaleFactor);
51+
const scaledHeight = Math.floor(imageHeight * scaleFactor);
52+
53+
return (
54+
<Root
55+
style={{
56+
width: scaledWidth + 'px',
57+
height: scaledHeight + 'px'
58+
}}
59+
>
60+
<div
4961
style={{
50-
width: scaledWidth + 'px',
51-
height: scaledHeight + 'px'
62+
left: (-(imageWidth - scaledWidth) / 2) + 'px',
63+
top: (-(imageHeight - scaledHeight) / 2) + 'px',
64+
width: imageWidth + 'px',
65+
height: imageHeight + 'px',
66+
transform: `scale(${scaleFactor})`
5267
}}
68+
ref={wrapper}
5369
>
54-
<div
55-
style={{
56-
left: (-(imageWidth - scaledWidth) / 2) + 'px',
57-
top: (-(imageHeight - scaledHeight) / 2) + 'px',
58-
width: imageWidth + 'px',
59-
height: imageHeight + 'px',
60-
transform: `scale(${scaleFactor})`
61-
}}
62-
>
63-
{textZones.map((zone, i) => (
64-
<TextZone
65-
key={i}
66-
style={{
67-
left: zone.x + 'px',
68-
bottom: zone.y + 'px',
69-
width: zone.width + 'px',
70-
height: zone.height + 'px',
71-
fontSize: zone.height * 0.9 + 'px'
72-
}}
73-
>
74-
{zone.text}
75-
</TextZone>
76-
))}
77-
</div>
78-
</Root>
79-
);
80-
}
70+
{textZones.map((zone, i) => (
71+
<TextZone
72+
key={i}
73+
style={{
74+
left: zone.x + 'px',
75+
bottom: zone.y + 'px',
76+
width: zone.width + 'px',
77+
height: zone.height + 'px',
78+
fontSize: zone.height * 0.9 + 'px'
79+
}}
80+
>
81+
<span>{zone.text}</span>
82+
</TextZone>
83+
))}
84+
</div>
85+
</Root>
86+
);
8187
}
8288

8389
export default TextLayer;

0 commit comments

Comments
 (0)