add DPI options and some scaling
TODO: the layout still wrong
This commit is contained in:
parent
d13cd3b2c8
commit
9483f369d7
@ -1,170 +0,0 @@
|
||||
# Testing Scenarios
|
||||
|
||||
## Current Status
|
||||
|
||||
The code is working! It correctly detects when no Bluetooth printer is configured and attempts to fall back to standard print.
|
||||
|
||||
## Enhanced Logging
|
||||
|
||||
Added comprehensive logging to track:
|
||||
1. When `originalPrintHtml` is called
|
||||
2. What it returns
|
||||
3. Any errors that occur
|
||||
4. Connection status checks
|
||||
5. Reconnection attempts
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### Scenario 1: No Bluetooth Printer Configured ✅
|
||||
**Current behavior:**
|
||||
```
|
||||
[BluetoothPrint] printHtml() called
|
||||
[BluetoothPrint] Web Bluetooth API available
|
||||
[BluetoothPrint] No Bluetooth printer configured, using standard print
|
||||
[BluetoothPrint] Calling originalPrintHtml with: [element]
|
||||
[BluetoothPrint] originalPrintHtml returned: [result]
|
||||
```
|
||||
|
||||
**Expected:** Browser print dialog should open
|
||||
**If not working:** Check the console for errors from `originalPrintHtml`
|
||||
|
||||
### Scenario 2: Bluetooth Printer Configured but Not Connected
|
||||
**Steps:**
|
||||
1. Connect to printer once (saves config)
|
||||
2. Disconnect printer or turn it off
|
||||
3. Try to print receipt
|
||||
|
||||
**Expected logs:**
|
||||
```
|
||||
[BluetoothPrint] Bluetooth printer configured: RPP02N
|
||||
[BluetoothPrint] Current connection status: disconnected
|
||||
[BluetoothPrint] Printer not connected, attempting to reconnect...
|
||||
[BluetoothPrint] Reconnection failed: [error]
|
||||
[BluetoothPrint] Falling back to standard print
|
||||
```
|
||||
|
||||
**Expected:** Browser print dialog should open
|
||||
|
||||
### Scenario 3: Bluetooth Printer Connected ✅
|
||||
**Steps:**
|
||||
1. Click Bluetooth icon
|
||||
2. Connect to RPP02N
|
||||
3. Make a sale and print
|
||||
|
||||
**Expected logs:**
|
||||
```
|
||||
[BluetoothPrint] Bluetooth printer configured: RPP02N
|
||||
[BluetoothPrint] Current connection status: connected
|
||||
[BluetoothPrint] Attempting bluetooth print...
|
||||
[BluetoothPrint] Starting bluetooth print from HTML...
|
||||
[BluetoothPrint] Parsing receipt data from HTML...
|
||||
[BluetoothPrint] Parsed X lines from HTML
|
||||
[BluetoothPrint] Generating ESC/POS commands...
|
||||
[BluetoothPrint] Generated XXX bytes of ESC/POS data
|
||||
[BluetoothPrint] Sending to printer...
|
||||
[BluetoothPrint] Print completed successfully
|
||||
```
|
||||
|
||||
**Expected:** Receipt prints to Bluetooth printer
|
||||
|
||||
### Scenario 4: Bluetooth Print Fails Mid-Process
|
||||
**What happens:** Connection drops during print
|
||||
|
||||
**Expected logs:**
|
||||
```
|
||||
[BluetoothPrint] Attempting bluetooth print...
|
||||
[BluetoothPrint] Bluetooth print failed: [error]
|
||||
[BluetoothPrint] Falling back to standard print after error
|
||||
[BluetoothPrint] Fallback print returned: [result]
|
||||
```
|
||||
|
||||
**Expected:** Browser print dialog opens as fallback
|
||||
|
||||
## What to Test Now
|
||||
|
||||
### Test 1: Connect Bluetooth Printer
|
||||
1. Update module: `./odoo-bin -u pos_bluetooth_thermal_printer -d your_database`
|
||||
2. Clear browser cache
|
||||
3. Click Bluetooth icon in POS
|
||||
4. Scan and connect to RPP02N
|
||||
5. Make a sale and print receipt
|
||||
6. **Check console logs**
|
||||
7. **Verify receipt prints to Bluetooth printer**
|
||||
|
||||
### Test 2: Standard Print Fallback
|
||||
1. Disconnect Bluetooth printer (turn off or unpair)
|
||||
2. Make a sale and print receipt
|
||||
3. **Check console logs** - should show reconnection attempt
|
||||
4. **Verify browser print dialog opens**
|
||||
|
||||
## Debugging Standard Print Issue
|
||||
|
||||
If standard print (browser dialog) doesn't open, check:
|
||||
|
||||
### 1. Check Console for Errors
|
||||
Look for:
|
||||
```
|
||||
[BluetoothPrint] Error calling originalPrintHtml: [error]
|
||||
[BluetoothPrint] Standard print failed: [error]
|
||||
[BluetoothPrint] Fallback print also failed: [error]
|
||||
```
|
||||
|
||||
### 2. Check originalPrintHtml Return Value
|
||||
Look for:
|
||||
```
|
||||
[BluetoothPrint] originalPrintHtml returned: [value]
|
||||
```
|
||||
|
||||
If it returns `undefined` or `null`, the original method might not be opening the print dialog.
|
||||
|
||||
### 3. Possible Issues
|
||||
|
||||
**Issue A: Original method doesn't exist**
|
||||
- `originalPrintHtml` might be `undefined`
|
||||
- Check if `PosPrinterService.prototype.printHtml` exists before patching
|
||||
|
||||
**Issue B: Original method needs different context**
|
||||
- Might need to pass additional parameters
|
||||
- Might need different `this` binding
|
||||
|
||||
**Issue C: Browser blocks print dialog**
|
||||
- Some browsers block print dialogs not triggered by user action
|
||||
- Might need user interaction first
|
||||
|
||||
### 4. Alternative Fallback
|
||||
|
||||
If `originalPrintHtml` doesn't work, we can implement a direct print fallback:
|
||||
|
||||
```javascript
|
||||
// Direct browser print as last resort
|
||||
const printWindow = window.open('', '_blank');
|
||||
printWindow.document.write(el.outerHTML);
|
||||
printWindow.document.close();
|
||||
printWindow.print();
|
||||
```
|
||||
|
||||
## Expected Results After Update
|
||||
|
||||
### With Bluetooth Printer Connected:
|
||||
✅ Receipt prints to Bluetooth printer automatically
|
||||
✅ No browser print dialog
|
||||
✅ Console shows successful print process
|
||||
|
||||
### Without Bluetooth Printer:
|
||||
✅ Console shows "No Bluetooth printer configured"
|
||||
✅ Falls back to standard print
|
||||
✅ Browser print dialog opens (or should open)
|
||||
|
||||
### With Bluetooth Error:
|
||||
✅ Console shows error details
|
||||
✅ Falls back to standard print
|
||||
✅ Sale completes successfully
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Update module**
|
||||
2. **Test with Bluetooth printer connected** - Should print!
|
||||
3. **Test without Bluetooth printer** - Check if standard print works
|
||||
4. **Share console logs** if any issues
|
||||
|
||||
The Bluetooth printing should work now when printer is connected! 🎉
|
||||
@ -39,6 +39,7 @@ export class BluetoothPrinterConfig extends Component {
|
||||
characterSet: 'CP437',
|
||||
paperWidth: 48,
|
||||
paperWidthMm: 58, // Paper width in millimeters (58mm or 80mm)
|
||||
dpi: 203, // Printer DPI (203 or 304)
|
||||
autoReconnect: true,
|
||||
timeout: 10000,
|
||||
|
||||
@ -83,6 +84,7 @@ export class BluetoothPrinterConfig extends Component {
|
||||
this.state.characterSet = config.settings.characterSet || 'CP437';
|
||||
this.state.paperWidth = config.settings.paperWidth || 48;
|
||||
this.state.paperWidthMm = config.settings.paperWidthMm || 58;
|
||||
this.state.dpi = config.settings.dpi || 203;
|
||||
this.state.autoReconnect = config.settings.autoReconnect !== false;
|
||||
this.state.timeout = config.settings.timeout || 10000;
|
||||
}
|
||||
@ -287,6 +289,15 @@ export class BluetoothPrinterConfig extends Component {
|
||||
this._saveConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle DPI change
|
||||
* @param {Event} event - Change event
|
||||
*/
|
||||
onDpiChange(event) {
|
||||
this.state.dpi = parseInt(event.target.value, 10);
|
||||
this._saveConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle auto-reconnect toggle
|
||||
* @param {Event} event - Change event
|
||||
@ -325,6 +336,7 @@ export class BluetoothPrinterConfig extends Component {
|
||||
characterSet: this.state.characterSet,
|
||||
paperWidth: this.state.paperWidth,
|
||||
paperWidthMm: this.state.paperWidthMm,
|
||||
dpi: this.state.dpi,
|
||||
autoReconnect: this.state.autoReconnect,
|
||||
timeout: this.state.timeout
|
||||
}
|
||||
|
||||
@ -5,6 +5,11 @@
|
||||
*
|
||||
* Converts HTML receipt elements to images for thermal printer graphics mode.
|
||||
* Uses canvas to render HTML and convert to bitmap format.
|
||||
*
|
||||
* FIXED: Improved rendering to preserve exact HTML layout, use 100% paper width,
|
||||
* and properly render images, borders, backgrounds, and text alignment.
|
||||
* The improved method uses getBoundingClientRect() to calculate actual element
|
||||
* positions and renders them with full styling preservation.
|
||||
*/
|
||||
|
||||
export class HtmlToImageConverter {
|
||||
@ -13,8 +18,8 @@ export class HtmlToImageConverter {
|
||||
// 58mm paper: 384 pixels (48mm printable * 8 dots/mm)
|
||||
// 80mm paper: 576 pixels (72mm printable * 8 dots/mm)
|
||||
this.paperWidthMm = 58;
|
||||
this.paperWidth = 384; // Default for 58mm
|
||||
this.dpi = 203; // Typical thermal printer DPI
|
||||
this.paperWidth = 464; // Default for 58mm (full width)
|
||||
this.dpi = 203; // Default thermal printer DPI (can be 203 or 304)
|
||||
this.scale = 2; // Higher scale for better quality
|
||||
}
|
||||
|
||||
@ -37,42 +42,100 @@ export class HtmlToImageConverter {
|
||||
clone.style.maxWidth = `${this.paperWidth}px`;
|
||||
clone.style.minWidth = `${this.paperWidth}px`;
|
||||
clone.style.boxSizing = 'border-box';
|
||||
clone.style.padding = '10px';
|
||||
clone.style.margin = '0';
|
||||
clone.style.padding = '0';
|
||||
clone.style.backgroundColor = 'white';
|
||||
clone.style.color = 'black';
|
||||
clone.style.fontFamily = 'monospace, Courier, "Courier New"';
|
||||
clone.style.fontSize = '12px';
|
||||
clone.style.lineHeight = '1.4';
|
||||
clone.style.overflow = 'visible';
|
||||
|
||||
// Scale all fonts by 100% (2x) to match test print readability
|
||||
// Test print uses ESC/POS text mode which has larger fonts
|
||||
const fontScale = 2.0;
|
||||
console.log('[HtmlToImage] Scaling fonts by', fontScale, 'x to match test print readability');
|
||||
|
||||
const allElements = clone.querySelectorAll('*');
|
||||
allElements.forEach(element => {
|
||||
const style = window.getComputedStyle(element);
|
||||
const currentFontSize = parseFloat(style.fontSize);
|
||||
|
||||
if (currentFontSize > 0) {
|
||||
const newFontSize = currentFontSize * fontScale;
|
||||
element.style.fontSize = `${newFontSize}px`;
|
||||
}
|
||||
|
||||
// Also reduce padding to prevent overflow and cropping
|
||||
const paddingLeft = parseFloat(style.paddingLeft) || 0;
|
||||
const paddingRight = parseFloat(style.paddingRight) || 0;
|
||||
|
||||
// If padding is excessive, reduce it
|
||||
if (paddingLeft > 8) {
|
||||
element.style.paddingLeft = '4px';
|
||||
}
|
||||
if (paddingRight > 8) {
|
||||
element.style.paddingRight = '4px';
|
||||
}
|
||||
});
|
||||
|
||||
// Also scale the root element font size
|
||||
const rootFontSize = parseFloat(window.getComputedStyle(clone).fontSize);
|
||||
if (rootFontSize > 0) {
|
||||
clone.style.fontSize = `${rootFontSize * fontScale}px`;
|
||||
}
|
||||
|
||||
// Reduce root padding to prevent cropping
|
||||
clone.style.paddingLeft = '4px';
|
||||
clone.style.paddingRight = '4px';
|
||||
|
||||
console.log('[HtmlToImage] Fonts scaled 2x, padding reduced, rendering at paper width:', this.paperWidth, 'px');
|
||||
|
||||
// Create a temporary container to measure height
|
||||
// Container must be large enough to hold the scaled content
|
||||
const container = document.createElement('div');
|
||||
container.style.position = 'fixed';
|
||||
container.style.left = '0';
|
||||
container.style.left = '-9999px'; // Move off-screen instead of using opacity
|
||||
container.style.top = '0';
|
||||
container.style.width = `${this.paperWidth}px`;
|
||||
container.style.maxWidth = `${this.paperWidth}px`;
|
||||
container.style.minWidth = `${this.paperWidth}px`;
|
||||
container.style.backgroundColor = 'white';
|
||||
container.style.overflow = 'visible';
|
||||
container.style.zIndex = '-1000';
|
||||
container.style.opacity = '0';
|
||||
container.style.boxSizing = 'border-box';
|
||||
|
||||
container.appendChild(clone);
|
||||
document.body.appendChild(container);
|
||||
|
||||
try {
|
||||
// Wait for layout to settle and fonts to load
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
// Wait for layout to settle, fonts to load, and images to load
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
// Load all images in the clone
|
||||
const images = clone.querySelectorAll('img');
|
||||
await Promise.all(Array.from(images).map(img => {
|
||||
return new Promise((resolve) => {
|
||||
if (img.complete) {
|
||||
resolve();
|
||||
} else {
|
||||
img.onload = resolve;
|
||||
img.onerror = resolve; // Continue even if image fails
|
||||
// Timeout after 2 seconds
|
||||
setTimeout(resolve, 2000);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// Get the actual rendered dimensions
|
||||
const rect = container.getBoundingClientRect();
|
||||
const rect = clone.getBoundingClientRect();
|
||||
const width = this.paperWidth;
|
||||
const height = Math.max(Math.ceil(rect.height), 100);
|
||||
const height = Math.ceil(rect.height);
|
||||
|
||||
console.log('[HtmlToImage] Measured dimensions:', width, 'x', height);
|
||||
console.log('[HtmlToImage] Rendered dimensions:', rect.width, 'x', rect.height);
|
||||
console.log('[HtmlToImage] Final canvas size:', width, 'x', height);
|
||||
console.log('[HtmlToImage] Container rect:', rect);
|
||||
console.log('[HtmlToImage] Receipt HTML preview (first 1000 chars):', clone.outerHTML.substring(0, 1000));
|
||||
console.log('[HtmlToImage] Receipt text content:', clone.textContent.substring(0, 500));
|
||||
|
||||
// Create canvas
|
||||
// Create canvas with exact dimensions
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
@ -82,9 +145,10 @@ export class HtmlToImageConverter {
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Render the DOM to canvas manually
|
||||
// Render the DOM to canvas using improved method
|
||||
// Content is already at the correct width, no scaling needed
|
||||
console.log('[HtmlToImage] Rendering DOM to canvas...');
|
||||
await this._renderDomToCanvas(clone, ctx, width, height);
|
||||
await this._renderDomToCanvasImproved(clone, ctx, 0, 0, width, height);
|
||||
|
||||
console.log('[HtmlToImage] Canvas rendering complete');
|
||||
return canvas;
|
||||
@ -96,7 +160,258 @@ export class HtmlToImageConverter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render DOM element to canvas - Simple and reliable approach
|
||||
* Render DOM element to canvas - Improved method that preserves layout
|
||||
* Recursively renders elements with proper positioning, styling, and images
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLElement} element - Element to render
|
||||
* @param {CanvasRenderingContext2D} ctx - Canvas context
|
||||
* @param {number} offsetX - X offset for rendering
|
||||
* @param {number} offsetY - Y offset for rendering
|
||||
* @param {number} canvasWidth - Canvas width
|
||||
* @param {number} canvasHeight - Canvas height
|
||||
* @returns {Promise<number>} Height used
|
||||
*/
|
||||
async _renderDomToCanvasImproved(element, ctx, offsetX, offsetY, canvasWidth, canvasHeight) {
|
||||
console.log('[HtmlToImage] Rendering DOM to canvas (improved method)...');
|
||||
|
||||
// Background is already filled, no need to fill again
|
||||
|
||||
// Get the bounding rect of the root element to calculate offsets
|
||||
const rootRect = element.getBoundingClientRect();
|
||||
|
||||
// Render element recursively with proper offset calculation
|
||||
await this._renderElement(element, ctx, -rootRect.left, -rootRect.top, rootRect);
|
||||
|
||||
console.log('[HtmlToImage] Rendering complete');
|
||||
return canvasHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively render an element and its children
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLElement} element - Element to render
|
||||
* @param {CanvasRenderingContext2D} ctx - Canvas context
|
||||
* @param {number} offsetX - X offset
|
||||
* @param {number} offsetY - Y offset
|
||||
* @param {DOMRect} rootRect - Root element bounding rect for reference
|
||||
*/
|
||||
async _renderElement(element, ctx, offsetX, offsetY, rootRect) {
|
||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const style = window.getComputedStyle(element);
|
||||
|
||||
// Skip hidden elements
|
||||
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
const x = rect.left + offsetX;
|
||||
const y = rect.top + offsetY;
|
||||
|
||||
// Render background
|
||||
const bgColor = style.backgroundColor;
|
||||
if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') {
|
||||
ctx.fillStyle = bgColor;
|
||||
ctx.fillRect(x, y, rect.width, rect.height);
|
||||
}
|
||||
|
||||
// Render borders
|
||||
this._renderBorders(ctx, x, y, rect.width, rect.height, style);
|
||||
|
||||
// Render images
|
||||
if (element.tagName === 'IMG') {
|
||||
await this._renderImage(element, ctx, x, y, rect.width, rect.height);
|
||||
}
|
||||
|
||||
// Render text content
|
||||
if (element.childNodes.length > 0) {
|
||||
const hasOnlyText = Array.from(element.childNodes).every(
|
||||
node => node.nodeType === Node.TEXT_NODE || (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR')
|
||||
);
|
||||
|
||||
if (hasOnlyText) {
|
||||
this._renderText(element, ctx, x, y, rect.width, rect.height, style);
|
||||
} else {
|
||||
// Render children recursively
|
||||
for (const child of element.children) {
|
||||
await this._renderElement(child, ctx, offsetX, offsetY, rootRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render text content of an element
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLElement} element - Element containing text
|
||||
* @param {CanvasRenderingContext2D} ctx - Canvas context
|
||||
* @param {number} x - X position
|
||||
* @param {number} y - Y position
|
||||
* @param {number} width - Element width
|
||||
* @param {number} height - Element height
|
||||
* @param {CSSStyleDeclaration} style - Computed style
|
||||
*/
|
||||
_renderText(element, ctx, x, y, width, height, style) {
|
||||
const text = element.textContent.trim();
|
||||
if (!text) return;
|
||||
|
||||
// Set font
|
||||
const fontSize = parseFloat(style.fontSize) || 12;
|
||||
const fontWeight = style.fontWeight;
|
||||
const fontFamily = style.fontFamily.split(',')[0].replace(/['"]/g, '') || 'monospace';
|
||||
const isBold = fontWeight === 'bold' || parseInt(fontWeight) >= 600;
|
||||
|
||||
ctx.font = `${isBold ? 'bold' : 'normal'} ${fontSize}px ${fontFamily}`;
|
||||
ctx.fillStyle = style.color || 'black';
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
// Handle text alignment
|
||||
const textAlign = style.textAlign || 'left';
|
||||
const paddingLeft = parseFloat(style.paddingLeft || 0);
|
||||
const paddingRight = parseFloat(style.paddingRight || 0);
|
||||
const paddingTop = parseFloat(style.paddingTop || 0);
|
||||
|
||||
let textX = x + paddingLeft;
|
||||
|
||||
if (textAlign === 'center') {
|
||||
ctx.textAlign = 'center';
|
||||
textX = x + width / 2;
|
||||
} else if (textAlign === 'right') {
|
||||
ctx.textAlign = 'right';
|
||||
textX = x + width - paddingRight;
|
||||
} else {
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
|
||||
const textY = y + paddingTop;
|
||||
|
||||
// Word wrap if needed
|
||||
const maxWidth = width - paddingLeft - paddingRight;
|
||||
const lineHeight = parseFloat(style.lineHeight) || fontSize * 1.4;
|
||||
|
||||
this._wrapAndDrawText(ctx, text, textX, textY, maxWidth, lineHeight);
|
||||
|
||||
// Reset text align
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap and draw text with proper line breaks
|
||||
*
|
||||
* @private
|
||||
* @param {CanvasRenderingContext2D} ctx - Canvas context
|
||||
* @param {string} text - Text to draw
|
||||
* @param {number} x - X position
|
||||
* @param {number} y - Y position
|
||||
* @param {number} maxWidth - Maximum width
|
||||
* @param {number} lineHeight - Line height
|
||||
*/
|
||||
_wrapAndDrawText(ctx, text, x, y, maxWidth, lineHeight) {
|
||||
const words = text.split(' ');
|
||||
let line = '';
|
||||
let currentY = y;
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const testLine = line + (line ? ' ' : '') + words[i];
|
||||
const metrics = ctx.measureText(testLine);
|
||||
|
||||
if (metrics.width > maxWidth && line) {
|
||||
ctx.fillText(line, x, currentY);
|
||||
line = words[i];
|
||||
currentY += lineHeight;
|
||||
} else {
|
||||
line = testLine;
|
||||
}
|
||||
}
|
||||
|
||||
if (line) {
|
||||
ctx.fillText(line, x, currentY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render borders of an element
|
||||
*
|
||||
* @private
|
||||
* @param {CanvasRenderingContext2D} ctx - Canvas context
|
||||
* @param {number} x - X position
|
||||
* @param {number} y - Y position
|
||||
* @param {number} width - Element width
|
||||
* @param {number} height - Element height
|
||||
* @param {CSSStyleDeclaration} style - Computed style
|
||||
*/
|
||||
_renderBorders(ctx, x, y, width, height, style) {
|
||||
const borderTop = parseFloat(style.borderTopWidth) || 0;
|
||||
const borderRight = parseFloat(style.borderRightWidth) || 0;
|
||||
const borderBottom = parseFloat(style.borderBottomWidth) || 0;
|
||||
const borderLeft = parseFloat(style.borderLeftWidth) || 0;
|
||||
|
||||
if (borderTop > 0) {
|
||||
ctx.strokeStyle = style.borderTopColor || 'black';
|
||||
ctx.lineWidth = borderTop;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x + width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
if (borderRight > 0) {
|
||||
ctx.strokeStyle = style.borderRightColor || 'black';
|
||||
ctx.lineWidth = borderRight;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + width, y);
|
||||
ctx.lineTo(x + width, y + height);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
if (borderBottom > 0) {
|
||||
ctx.strokeStyle = style.borderBottomColor || 'black';
|
||||
ctx.lineWidth = borderBottom;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y + height);
|
||||
ctx.lineTo(x + width, y + height);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
if (borderLeft > 0) {
|
||||
ctx.strokeStyle = style.borderLeftColor || 'black';
|
||||
ctx.lineWidth = borderLeft;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x, y + height);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an image element
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLImageElement} img - Image element
|
||||
* @param {CanvasRenderingContext2D} ctx - Canvas context
|
||||
* @param {number} x - X position
|
||||
* @param {number} y - Y position
|
||||
* @param {number} width - Display width
|
||||
* @param {number} height - Display height
|
||||
*/
|
||||
async _renderImage(img, ctx, x, y, width, height) {
|
||||
try {
|
||||
if (img.complete && img.naturalWidth > 0) {
|
||||
ctx.drawImage(img, x, y, width, height);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[HtmlToImage] Failed to render image:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render DOM element to canvas - Simple fallback method
|
||||
* Extracts text content and renders it line by line with proper formatting
|
||||
*
|
||||
* @private
|
||||
@ -106,7 +421,7 @@ export class HtmlToImageConverter {
|
||||
* @param {number} height - Canvas height
|
||||
*/
|
||||
async _renderDomToCanvas(element, ctx, width, height) {
|
||||
console.log('[HtmlToImage] Rendering DOM to canvas (simple method)...');
|
||||
console.log('[HtmlToImage] Rendering DOM to canvas (simple fallback method)...');
|
||||
|
||||
// Set default styles
|
||||
ctx.fillStyle = 'white';
|
||||
@ -356,28 +671,34 @@ export class HtmlToImageConverter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set paper width in millimeters
|
||||
* Set paper width in millimeters and DPI
|
||||
*
|
||||
* @param {number} widthMm - Paper width in millimeters (58 or 80)
|
||||
* @param {number} dpi - Printer DPI (203 or 304), defaults to 203
|
||||
*/
|
||||
setPaperWidth(widthMm) {
|
||||
setPaperWidth(widthMm, dpi = 203) {
|
||||
this.paperWidthMm = widthMm;
|
||||
this.dpi = dpi;
|
||||
|
||||
// Calculate pixel width based on paper size
|
||||
// Thermal printers: 8 dots per mm
|
||||
// Account for margins (5mm on each side)
|
||||
// Calculate dots per mm based on DPI
|
||||
// 203 DPI = 8 dots/mm (203 / 25.4)
|
||||
// 304 DPI = 12 dots/mm (304 / 25.4)
|
||||
const dotsPerMm = Math.round(dpi / 25.4);
|
||||
|
||||
// Calculate pixel width based on paper size and DPI
|
||||
// Use FULL width without margins for maximum usage
|
||||
if (widthMm === 58) {
|
||||
// 58mm paper: 48mm printable width = 384 pixels
|
||||
this.paperWidth = 384;
|
||||
// 58mm paper at selected DPI
|
||||
this.paperWidth = widthMm * dotsPerMm;
|
||||
} else if (widthMm === 80) {
|
||||
// 80mm paper: 72mm printable width = 576 pixels
|
||||
this.paperWidth = 576;
|
||||
// 80mm paper at selected DPI
|
||||
this.paperWidth = widthMm * dotsPerMm;
|
||||
} else {
|
||||
// Custom width: use full width minus 10mm margins
|
||||
this.paperWidth = (widthMm - 10) * 8;
|
||||
// Custom width: use full width
|
||||
this.paperWidth = widthMm * dotsPerMm;
|
||||
}
|
||||
|
||||
console.log('[HtmlToImage] Setting paper width to', widthMm, 'mm (', this.paperWidth, 'pixels)');
|
||||
console.log('[HtmlToImage] Setting paper width to', widthMm, 'mm at', dpi, 'DPI (', dotsPerMm, 'dots/mm,', this.paperWidth, 'pixels)');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -324,12 +324,14 @@ patch(PosPrinterService.prototype, {
|
||||
const converter = new HtmlToImageConverter();
|
||||
console.log('[BluetoothPrint] HtmlToImageConverter created successfully');
|
||||
|
||||
// Get paper width from configuration
|
||||
// Get paper width and DPI from configuration
|
||||
const paperWidthMm = config?.settings?.paperWidthMm || 58;
|
||||
const dpi = config?.settings?.dpi || 203;
|
||||
console.log('[BluetoothPrint] Using paper width:', paperWidthMm, 'mm');
|
||||
console.log('[BluetoothPrint] Setting paper width on converter...');
|
||||
converter.setPaperWidth(paperWidthMm);
|
||||
console.log('[BluetoothPrint] Paper width set successfully');
|
||||
console.log('[BluetoothPrint] Using printer DPI:', dpi);
|
||||
console.log('[BluetoothPrint] Setting paper width and DPI on converter...');
|
||||
converter.setPaperWidth(paperWidthMm, dpi);
|
||||
console.log('[BluetoothPrint] Paper width and DPI set successfully');
|
||||
|
||||
console.log('[BluetoothPrint] Converting HTML to bitmap...');
|
||||
const bitmap = await converter.htmlToBitmap(el);
|
||||
|
||||
@ -127,6 +127,21 @@
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Printer DPI -->
|
||||
<div class="form-group">
|
||||
<label for="dpi">Printer Resolution (DPI)</label>
|
||||
<select id="dpi"
|
||||
class="form-control"
|
||||
t-model="state.dpi"
|
||||
t-on-change="onDpiChange">
|
||||
<option value="203">203 DPI (Standard)</option>
|
||||
<option value="304">304 DPI (High Resolution)</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">
|
||||
Select your printer's resolution. Most thermal printers use 203 DPI. Use 304 DPI for higher quality printers.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Auto Reconnect -->
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user