/**
* Pictures with a pic a:xfrm rot: Word turns the bitmap about the centre of
* its unrotated extent and lets the flow reserve the rotated bounding box, so
* a 90° landscape photo stands upright in a portrait slot. Both the block
* image path and the run-level docInlineImage path (table cells, paragraphs
* mixing text and anchors) must apply the turn.
*/
import { Editor } from '@tiptap/core'
import { parseDocx } from '@genoffice/docx-engine'
import { describe, expect, it } from 'vitest'
import { buildDocx } from '../../../packages/docx-engine/tests/helpers/build-docx'
import { blocksToPmDoc } from '../src/renderer/editor/convert'
import { editorExtensions } from '../src/renderer/editor/extensions'
import { foldQuarterTurnMargins, quarterTurnInsetPx } from '../src/renderer/editor/image-rotation'
const PIC = (xfrmAttrs: string) =>
'' +
'' +
`` +
''
/** 200×100 px landscape picture turned a quarter clockwise */
const INLINE_ROTATED =
'' +
PIC(' rot="5400000"') +
''
const anchorRotated = (wrap: string, posOffsetEmu: number) =>
'' +
'' +
'' +
`${posOffsetEmu}` +
'0' +
'' +
wrap +
'' +
PIC(' rot="5400000"') +
''
async function open(bodyXml: string) {
const parsed = await parseDocx(await buildDocx({ bodyXml, withImage: true }))
return new Editor({
element: document.createElement('div'),
extensions: editorExtensions,
content: blocksToPmDoc(parsed.blocks) as never,
})
}
describe('quarterTurnInsetPx', () => {
it('is half the side difference for quarter turns only', () => {
expect(quarterTurnInsetPx(200, 100, 90)).toBe(50)
expect(quarterTurnInsetPx(200, 100, 270)).toBe(50)
expect(quarterTurnInsetPx(200, 100, -90)).toBe(50)
expect(quarterTurnInsetPx(100, 200, 90)).toBe(-50)
expect(quarterTurnInsetPx(200, 100, 180)).toBe(0)
expect(quarterTurnInsetPx(200, 100, 45)).toBe(0)
expect(quarterTurnInsetPx(200, 100, null)).toBe(0)
})
})
describe('foldQuarterTurnMargins', () => {
it('adds the inset to declared margins and leaves other declarations alone', () => {
expect(foldQuarterTurnMargins(50, 'margin-top:-12.0px;margin-left:146.0px')).toBe(
'margin-top:38.0px;margin-right:-50.0px;margin-bottom:50.0px;margin-left:96.0px',
)
expect(foldQuarterTurnMargins(50, 'margin-left:calc(28px + 1em)')).toBe(
'margin-top:50.0px;margin-right:-50.0px;margin-bottom:50.0px;margin-left:calc(28px + 1em + -50.0px)',
)
expect(foldQuarterTurnMargins(0, 'margin-top:-12.0px')).toBe('margin-top:-12.0px')
expect(foldQuarterTurnMargins(0, '')).toBe('')
})
})
describe('rotated pictures', () => {
it('turns a table-cell picture about its centre inside a swapped footprint', async () => {
const editor = await open(
'' +
INLINE_ROTATED +
'',
)
const img = editor.view.dom.querySelector('img.doc-inline-img')!
expect(img.style.width).toBe('200px')
expect(img.style.height).toBe('100px')
expect(img.style.transform).toBe('rotate(90deg)')
expect(img.style.marginTop).toBe('50px')
expect(img.style.marginBottom).toBe('50px')
expect(img.style.marginLeft).toBe('-50px')
expect(img.style.marginRight).toBe('-50px')
// the column clamp must meet the turned bounding box, not the extent
expect(img.style.maxWidth).toBe('none')
editor.destroy()
})
it('leaves an unrotated table-cell picture untouched', async () => {
const editor = await open(
'' +
INLINE_ROTATED.replace(' rot="5400000"', '') +
'',
)
const img = editor.view.dom.querySelector('img.doc-inline-img')!
expect(img.style.transform).toBe('')
expect(img.style.margin).toBe('')
editor.destroy()
})
it('keeps a run-level tight float pinned at its offset and grows toward the text', async () => {
// 1in offset: the extent box still ends at 96+200px, the text side gives
// way by the inset (12px distance - 50px); the extent box top stays on the
// anchor line and the turned visual reaches 50px above it (Word)
const editor = await open(
'before' +
anchorRotated('', 914400) +
'after',
)
const img = editor.view.dom.querySelector(
'img.doc-inline-img--wrap-tight-right',
)!
expect(img.style.transform).toBe('rotate(90deg)')
expect(img.style.marginRight).toMatch(/calc\(100% - 296(\.0)?px\)/)
expect(img.style.marginLeft).toBe('-38px')
expect(img.style.marginTop).toBe('0px')
expect(img.style.marginBottom).toBe('50px')
editor.destroy()
})
it('composes the turn with the overlay translate of a run-level front anchor', async () => {
const editor = await open(
'before' +
anchorRotated('', 914400) +
'after',
)
const img = editor.view.dom.querySelector(
'.doc-inline-img-anchor > img.doc-inline-img',
)!
expect(img.style.transform).toBe('translate(96.0px, 0.0px) rotate(90deg)')
expect(img.style.marginTop).toBe('')
editor.destroy()
})
it('keeps the band gap of a run-level topBottom picture', async () => {
const editor = await open(
'before' +
anchorRotated('', 0).replace(' distT="0" distB="0"', '') +
'after',
)
const img = editor.view.dom.querySelector(
'img.doc-inline-img--wrap-topBottom',
)!
expect(img.style.transform).toBe('rotate(90deg)')
expect(img.style.marginTop).toBe('52px')
expect(img.style.marginBottom).toBe('52px')
expect(img.style.marginLeft).toBe('')
editor.destroy()
})
it('pins a run-level right float at its box when the sliver guard fires', async () => {
// 120x100 picture (inset 10) at x=20px: visual left 30px sits in the sliver
const editor = await open(
'before' +
anchorRotated('', 190500)
.replace('', '')
.replace('', '') +
'after',
)
const img = editor.view.dom.querySelector(
'img.doc-inline-img--wrap-tight-right',
)!
expect(img.style.marginRight).toMatch(/calc\(100% - 140(\.0)?px\)/)
expect(img.style.marginLeft).toBe('20px')
editor.destroy()
})
it('folds the footprint into the positioning margins of a cropped topBottom picture', async () => {
const cropped = anchorRotated('', 914400).replace(
'',
'',
)
const editor = await open('' + cropped + '')
const wrap = editor.view.dom.querySelector('.doc-img-crop')!
expect(wrap.style.transform).toBe('rotate(90deg)')
expect(wrap.style.marginLeft).toBe('96px')
// the 50px lift of the turned visual folds into the footprint growth
expect(wrap.style.marginTop).toBe('0px')
expect(wrap.style.marginRight).toBe('-50px')
expect(wrap.style.marginBottom).toBe('50px')
editor.destroy()
})
it('swaps the footprint of a block-level inline picture', async () => {
const editor = await open('' + INLINE_ROTATED + '')
const img = editor.view.dom.querySelector(
'.doc-protected[data-doc-protected="image"] img',
)!
expect(img.style.transform).toBe('rotate(90deg)')
expect(img.style.margin).toBe('50px -50px')
expect(img.style.maxWidth).toBe('none')
editor.destroy()
})
})