Skip to content

Commit

Permalink
feat: DPI (png、jpeg) can be set by scale when domToBlob or `domTo…
Browse files Browse the repository at this point in the history
…DataUrl`
  • Loading branch information
qq15725 committed Feb 21, 2023
1 parent a85e9ad commit 89e94bf
Show file tree
Hide file tree
Showing 32 changed files with 376 additions and 181 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

### Features

* support Web Worker and `placeholderImage` as a callback ([#5](https://github.com/qq15725/modern-screenshot/issues/5)) ([f22b0bc](https://github.com/qq15725/modern-screenshot/commit/f22b0bcf75d660637617af151ac95200773c0282))
* Support Web Worker ([#5](https://github.com/qq15725/modern-screenshot/issues/5)) ([f22b0bc](https://github.com/qq15725/modern-screenshot/commit/f22b0bcf75d660637617af151ac95200773c0282))



2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
document.body.appendChild(
await domToImage(
document.querySelector('#root'),
{ debug: true },
{ debug: true, scale: 2 },
),
)
</script>
Expand Down
8 changes: 8 additions & 0 deletions src/change-jpeg-dpi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function changeJpegDpi(uint8Array: Uint8Array, dpi: number) {
uint8Array[13] = 1 // 1 pixel per inch or 2 pixel per cm
uint8Array[14] = dpi >> 8 // dpiX high byte
uint8Array[15] = dpi & 0xFF // dpiX low byte
uint8Array[16] = dpi >> 8 // dpiY high byte
uint8Array[17] = dpi & 0xFF // dpiY low byte
return uint8Array
}
120 changes: 120 additions & 0 deletions src/change-png-dpi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
const _P = 'p'.charCodeAt(0)
const _H = 'H'.charCodeAt(0)
const _Y = 'Y'.charCodeAt(0)
const _S = 's'.charCodeAt(0)

let pngDataTable: ReturnType<typeof createPngDataTable>

function createPngDataTable() {
/* Table of CRCs of all 8-bit messages. */
const crcTable = new Int32Array(256)
for (let n = 0; n < 256; n++) {
let c = n
for (let k = 0; k < 8; k++) {
c = (c & 1) ? 0xEDB88320 ^ (c >>> 1) : c >>> 1
}
crcTable[n] = c
}
return crcTable
}

function calcCrc(uint8Array: Uint8Array) {
let c = -1
if (!pngDataTable) pngDataTable = createPngDataTable()
for (let n = 0; n < uint8Array.length; n++) {
c = pngDataTable[(c ^ uint8Array[n]) & 0xFF] ^ (c >>> 8)
}
return c ^ -1
}

function searchStartOfPhys(uint8Array: Uint8Array) {
const length = uint8Array.length - 1
// we check from the end since we cut the string in proximity of the header
// the header is within 21 bytes from the end.
for (let i = length; i >= 4; i--) {
if (uint8Array[i - 4] === 9 && uint8Array[i - 3] === _P
&& uint8Array[i - 2] === _H && uint8Array[i - 1] === _Y
&& uint8Array[i] === _S) {
return i - 3
}
}
return 0
}

export function changePngDpi(uint8Array: Uint8Array, dpi: number, overwritepHYs = false) {
const physChunk = new Uint8Array(13)
// chunk header pHYs
// 9 bytes of data
// 4 bytes of crc
// this multiplication is because the standard is dpi per meter.
dpi *= 39.3701
physChunk[0] = _P
physChunk[1] = _H
physChunk[2] = _Y
physChunk[3] = _S
physChunk[4] = dpi >>> 24 // dpiX highest byte
physChunk[5] = dpi >>> 16 // dpiX veryhigh byte
physChunk[6] = dpi >>> 8 // dpiX high byte
physChunk[7] = dpi & 0xFF // dpiX low byte
physChunk[8] = physChunk[4] // dpiY highest byte
physChunk[9] = physChunk[5] // dpiY veryhigh byte
physChunk[10] = physChunk[6] // dpiY high byte
physChunk[11] = physChunk[7] // dpiY low byte
physChunk[12] = 1 // dot per meter....

const crc = calcCrc(physChunk)

const crcChunk = new Uint8Array(4)
crcChunk[0] = crc >>> 24
crcChunk[1] = crc >>> 16
crcChunk[2] = crc >>> 8
crcChunk[3] = crc & 0xFF

if (overwritepHYs) {
const startingIndex = searchStartOfPhys(uint8Array)
uint8Array.set(physChunk, startingIndex)
uint8Array.set(crcChunk, startingIndex + 13)
return uint8Array
} else {
// i need to give back an array of data that is divisible by 3 so that
// dataurl encoding gives me integers, for luck this chunk is 17 + 4 = 21
// if it was we could add a text chunk contaning some info, untill desired
// length is met.

// chunk structur 4 bytes for length is 9
const chunkLength = new Uint8Array(4)
chunkLength[0] = 0
chunkLength[1] = 0
chunkLength[2] = 0
chunkLength[3] = 9

const finalHeader = new Uint8Array(54)
finalHeader.set(uint8Array, 0)
finalHeader.set(chunkLength, 33)
finalHeader.set(physChunk, 37)
finalHeader.set(crcChunk, 50)
return finalHeader
}
}

// those are 3 possible signature of the physBlock in base64.
// the pHYs signature block is preceed by the 4 bytes of lenght. The length of
// the block is always 9 bytes. So a phys block has always this signature:
// 0 0 0 9 p H Y s.
// However the data64 encoding aligns we will always find one of those 3 strings.
// this allow us to find this particular occurence of the pHYs block without
// converting from b64 back to string
const b64PhysSignature1 = 'AAlwSFlz'
const b64PhysSignature2 = 'AAAJcEhZ'
const b64PhysSignature3 = 'AAAACXBI'

export function detectPhysChunkFromDataUrl(dataUrl: string) {
let b64index = dataUrl.indexOf(b64PhysSignature1)
if (b64index === -1) {
b64index = dataUrl.indexOf(b64PhysSignature2)
}
if (b64index === -1) {
b64index = dataUrl.indexOf(b64PhysSignature3)
}
return b64index
}
2 changes: 1 addition & 1 deletion src/create-canvas-clone.ts → src/clone-canvas.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { consoleWarn, createImage } from './utils'

export function createCanvasClone<T extends HTMLCanvasElement>(
export function cloneCanvas<T extends HTMLCanvasElement>(
canvas: T,
): HTMLCanvasElement | HTMLImageElement {
if (canvas.ownerDocument) {
Expand Down
29 changes: 29 additions & 0 deletions src/clone-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { isCanvasElement, isIFrameElement, isImageElement, isVideoElement } from './utils'
import { cloneIframe } from './clone-iframe'
import { cloneCanvas } from './clone-canvas'
import { cloneVideo } from './clone-video'
import { cloneImage } from './clone-image'
import type { Context } from './context'

export function cloneElement<T extends HTMLElement | SVGElement>(
node: T,
context: Context,
): HTMLElement | SVGElement {
if (isCanvasElement(node)) {
return cloneCanvas(node)
}

if (isIFrameElement(node)) {
return cloneIframe(node, context)
}

if (isImageElement(node)) {
return cloneImage(node)
}

if (isVideoElement(node)) {
return cloneVideo(node)
}

return node.cloneNode(false) as T
}
2 changes: 1 addition & 1 deletion src/create-iframe-clone.ts → src/clone-iframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { cloneNode } from './clone-node'
import { consoleWarn } from './utils'
import type { Context } from './context'

export function createIframeClone<T extends HTMLIFrameElement>(
export function cloneIframe<T extends HTMLIFrameElement>(
iframe: T,
context: Context,
): HTMLIFrameElement | HTMLBodyElement {
Expand Down
2 changes: 1 addition & 1 deletion src/create-image-clone.ts → src/clone-image.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export function createImageClone<T extends HTMLImageElement>(
export function cloneImage<T extends HTMLImageElement>(
image: T,
): HTMLImageElement {
const clone = image.cloneNode(false) as T
Expand Down
4 changes: 2 additions & 2 deletions src/clone-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
isTextNode,
isVideoElement,
} from './utils'
import { createElementClone } from './create-element-clone'
import { cloneElement } from './clone-element'
import type { Context } from './context'

function appendChildNode<T extends Node>(
Expand Down Expand Up @@ -85,7 +85,7 @@ export function cloneNode<T extends Node>(
return ownerDocument.createComment(node.tagName.toLowerCase())
}

const clone = createElementClone(node, context)
const clone = cloneElement(node, context)
const cloneStyle = clone.style

copyCssStyles(node, style, clone, isRoot, context)
Expand Down
6 changes: 3 additions & 3 deletions src/create-video-clone.ts → src/clone-video.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createCanvasClone } from './create-canvas-clone'
import { cloneCanvas } from './clone-canvas'
import { consoleWarn, createImage } from './utils'

export function createVideoClone<T extends HTMLVideoElement>(
export function cloneVideo<T extends HTMLVideoElement>(
video: T,
): HTMLCanvasElement | HTMLImageElement | HTMLVideoElement {
if (video.ownerDocument) {
Expand All @@ -17,7 +17,7 @@ export function createVideoClone<T extends HTMLVideoElement>(
} catch (error) {
consoleWarn('Failed to clone video', error)
}
return createCanvasClone(canvas)
return cloneCanvas(canvas)
}

if (video.poster) {
Expand Down
16 changes: 16 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ export interface InternalContext<T extends Node> {
*/
__CONTEXT__: true

/**
* Logger
*/
log: {
time: (label: string) => void
timeEnd: (label: string) => void
warn: (...args: any[]) => void
}

/**
* Node
*/
Expand All @@ -28,6 +37,13 @@ export interface InternalContext<T extends Node> {
*/
ownerWindow?: Window

/**
* DPI
*
* scale === 1 ? null : 96 * scale
*/
dpi: number | null

/**
* The `style` element under the root `svg` element
*/
Expand Down
24 changes: 0 additions & 24 deletions src/converts/canvas-to-blob.ts

This file was deleted.

32 changes: 22 additions & 10 deletions src/converts/dom-to-blob.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
import { createContext } from '../create-context'
import { isContext } from '../utils'
import { changeJpegDpi } from '../change-jpeg-dpi'
import { changePngDpi } from '../change-png-dpi'
import { orCreateContext } from '../create-context'
import { blobToArrayBuffer, canvasToBlob } from '../utils'
import { domToCanvas } from './dom-to-canvas'
import { canvasToblob } from './canvas-to-blob'
import type { Context } from '../context'
import type { Options } from '../options'

export async function domToBlob<T extends Node>(node: T, options?: Options): Promise<Blob | null>
export async function domToBlob<T extends Node>(context: Context<T>): Promise<Blob | null>
export async function domToBlob<T extends Node>(node: T, options?: Options): Promise<Blob>
export async function domToBlob<T extends Node>(context: Context<T>): Promise<Blob>
export async function domToBlob(node: any, options?: any) {
const context = isContext(node)
? node
: await createContext(node, { ...options, autodestruct: true })
const { type, quality } = context
const context = await orCreateContext(node, options)
const { log, type, quality, dpi } = context
const canvas = await domToCanvas(context)
return await canvasToblob(canvas, { type, quality })
log.time('canvas to blob')
const blob = await canvasToBlob(canvas, type, quality)
if (['image/png', 'image/jpeg'].includes(type) && dpi) {
const arrayBuffer = await blobToArrayBuffer(blob.slice(0, 33))
let uint8Array = new Uint8Array(arrayBuffer)
if (type === 'image/png') {
uint8Array = changePngDpi(uint8Array, dpi)
} else if (type === 'image/jpeg') {
uint8Array = changeJpegDpi(uint8Array, dpi)
}
return new Blob([uint8Array, blob.slice(33)], { type })
}
log.timeEnd('canvas to blob')
return blob
}
10 changes: 4 additions & 6 deletions src/converts/dom-to-canvas.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { createContext, createStyleElement } from '../create-context'
import { createImage, isContext, svgToDataUrl } from '../utils'
import { createStyleElement, orCreateContext } from '../create-context'
import { createImage, svgToDataUrl } from '../utils'
import { imageToCanvas } from '../image-to-canvas'
import { domToForeignObjectSvg } from './dom-to-foreign-object-svg'
import { imageToCanvas } from './image-to-canvas'
import type { Context } from '../context'

import type { Options } from '../options'

export async function domToCanvas<T extends Node>(node: T, options?: Options): Promise<HTMLCanvasElement>
export async function domToCanvas<T extends Node>(context: Context<T>): Promise<HTMLCanvasElement>
export async function domToCanvas(node: any, options?: any) {
const context = isContext(node)
? node
: await createContext(node, { ...options, autodestruct: true })
const context = await orCreateContext(node, options)
const svg = await domToForeignObjectSvg(context)
const dataUrl = svgToDataUrl(svg)
if (!context.autodestruct) {
Expand Down
Loading

1 comment on commit 89e94bf

@qq15725
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

close #9

Please sign in to comment.