diff --git a/plugins/impex/psd/psd_additional_layer_info_block.cpp b/plugins/impex/psd/psd_additional_layer_info_block.cpp index c217f757e7..f83def914f 100644 --- a/plugins/impex/psd/psd_additional_layer_info_block.cpp +++ b/plugins/impex/psd/psd_additional_layer_info_block.cpp @@ -1,413 +1,416 @@ /* * Copyright (c) 2014 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "psd_additional_layer_info_block.h" #include #include #include #include #include #include #include PsdAdditionalLayerInfoBlock::PsdAdditionalLayerInfoBlock(const PSDHeader& header) : m_header(header) { } void PsdAdditionalLayerInfoBlock::setExtraLayerInfoBlockHandler(ExtraLayerInfoBlockHandler handler) { m_layerInfoBlockHandler = handler; } bool PsdAdditionalLayerInfoBlock::read(QIODevice *io) { bool result = true; try { readImpl(io); } catch (KisAslReaderUtils::ASLParseException &e) { error = e.what(); result = false; } return result; } void PsdAdditionalLayerInfoBlock::readImpl(QIODevice* io) { using namespace KisAslReaderUtils; QStringList longBlocks; if (m_header.version > 1) { longBlocks << "LMsk" << "Lr16" << "Layr" << "Mt16" << "Mtrn" << "Alph"; } while (!io->atEnd()) { { const quint32 refSignature1 = 0x3842494D; // '8BIM' in little-endian const quint32 refSignature2 = 0x38423634; // '8B64' in little-endian if (!TRY_READ_SIGNATURE_2OPS_EX(io, refSignature1, refSignature2)) { break; } } QString key = readFixedString(io); dbgFile << "found info block with key" << key; quint64 blockSize = GARBAGE_VALUE_MARK; if (longBlocks.contains(key)) { SAFE_READ_EX(io, blockSize); } else { quint32 size32; SAFE_READ_EX(io, size32); blockSize = size32; } // offset verifier will correct the position on the exit from // current namespace, including 'continue', 'return' and // exceptions. SETUP_OFFSET_VERIFIER(infoBlockEndVerifier, io, blockSize, 0); if (keys.contains(key)) { error = "Found duplicate entry for key "; continue; } keys << key; // TODO: Loading of 32 bit files is not supported yet if (key == "Lr16"/* || key == "Lr32"*/) { if (m_layerInfoBlockHandler) { int offset = m_header.version > 1 ? 8 : 4; io->seek(io->pos() - offset); m_layerInfoBlockHandler(io); } } else if (key == "SoCo") { } else if (key == "GdFl") { } else if (key == "PtFl") { } else if (key == "brit") { } else if (key == "levl") { } else if (key == "curv") { } else if (key == "expA") { } else if (key == "vibA") { } else if (key == "hue") { } else if (key == "hue2") { } else if (key == "blnc") { } else if (key == "blwh") { } else if (key == "phfl") { } else if (key == "mixr") { } else if (key == "clrL") { } else if (key == "nvrt") { } else if (key == "post") { } else if (key == "thrs") { } else if (key == "grdm") { } else if (key == "selc") { } else if (key == "lrFX") { // deprecated! use lfx2 instead! } else if (key == "tySh") { } else if (key == "luni") { // get the unicode layer name unicodeLayerName = readUnicodeString(io); dbgFile << "unicodeLayerName" << unicodeLayerName; } else if (key == "lyid") { } - else if (key == "lfx2") { + else if (key == "lfx2" || key == "lfxs") { + // lfxs is a special variant of layer styles for group layers + KisAslReader reader; layerStyleXml = reader.readLfx2PsdSection(io); } else if (key == "Patt" || key == "Pat2" || key == "Pat3") { KisAslReader reader; QDomDocument pattern = reader.readPsdSectionPattern(io, blockSize); embeddedPatterns << pattern; } else if (key == "Anno") { } else if (key == "clbl") { } else if (key == "infx") { } else if (key == "knko") { } else if (key == "spf") { } else if (key == "lclr") { } else if (key == "fxrp") { } else if (key == "grdm") { } else if (key == "lsct") { quint32 dividerType = GARBAGE_VALUE_MARK; SAFE_READ_EX(io, dividerType); this->sectionDividerType = (psd_section_type)dividerType; dbgFile << "Reading \"lsct\" block:"; dbgFile << ppVar(blockSize); dbgFile << ppVar(dividerType); if (blockSize >= 12) { quint32 lsctSignature = GARBAGE_VALUE_MARK; const quint32 refSignature1 = 0x3842494D; // '8BIM' in little-endian SAFE_READ_SIGNATURE_EX(io, lsctSignature, refSignature1); this->sectionDividerBlendMode = readFixedString(io); dbgFile << ppVar(this->sectionDividerBlendMode); } // Animation if (blockSize >= 14) { /** * "I don't care * I don't care, no... !" (c) */ } } else if (key == "brst") { } else if (key == "SoCo") { } else if (key == "PtFl") { } else if (key == "GdFl") { } else if (key == "vmsk" || key == "vsms") { // If key is "vsms" then we are writing for (Photoshop CS6) and the document will have a "vscg" key } else if (key == "TySh") { } else if (key == "ffxi") { } else if (key == "lnsr") { } else if (key == "shpa") { } else if (key == "shmd") { } else if (key == "lyvr") { } else if (key == "tsly") { } else if (key == "lmgm") { } else if (key == "vmgm") { } else if (key == "plLd") { // Replaced by SoLd in CS3 } else if (key == "linkD" || key == "lnk2" || key == "lnk3") { } else if (key == "phfl") { } else if (key == "blwh") { } else if (key == "CgEd") { } else if (key == "Txt2") { } else if (key == "vibA") { } else if (key == "pths") { } else if (key == "anFX") { } else if (key == "FMsk") { } else if (key == "SoLd") { } else if (key == "vstk") { } else if (key == "vsCg") { } else if (key == "sn2P") { } else if (key == "vogk") { } else if (key == "Mtrn" || key == "Mt16" || key == "Mt32") { // There is no data associated with these keys. } else if (key == "LMsk") { } else if (key == "expA") { } else if (key == "FXid") { } else if (key == "FEid") { } } } bool PsdAdditionalLayerInfoBlock::write(QIODevice */*io*/, KisNodeSP /*node*/) { return true; } bool PsdAdditionalLayerInfoBlock::valid() { return true; } void PsdAdditionalLayerInfoBlock::writeLuniBlockEx(QIODevice* io, const QString &layerName) { KisAslWriterUtils::writeFixedString("8BIM", io); KisAslWriterUtils::writeFixedString("luni", io); KisAslWriterUtils::OffsetStreamPusher layerNameSizeTag(io, 2); KisAslWriterUtils::writeUnicodeString(layerName, io); } void PsdAdditionalLayerInfoBlock::writeLsctBlockEx(QIODevice* io, psd_section_type sectionType, bool isPassThrough, const QString &blendModeKey) { KisAslWriterUtils::writeFixedString("8BIM", io); KisAslWriterUtils::writeFixedString("lsct", io); KisAslWriterUtils::OffsetStreamPusher sectionTypeSizeTag(io, 2); SAFE_WRITE_EX(io, (quint32)sectionType); QString realBlendModeKey = isPassThrough ? QString("pass") : blendModeKey; KisAslWriterUtils::writeFixedString("8BIM", io); KisAslWriterUtils::writeFixedString(realBlendModeKey, io); } -void PsdAdditionalLayerInfoBlock::writeLfx2BlockEx(QIODevice* io, const QDomDocument &stylesXmlDoc) +void PsdAdditionalLayerInfoBlock::writeLfx2BlockEx(QIODevice* io, const QDomDocument &stylesXmlDoc, bool useLfxsLayerStyleFormat) { KisAslWriterUtils::writeFixedString("8BIM", io); - KisAslWriterUtils::writeFixedString("lfx2", io); + // 'lfxs' format is used for Group layers in PS + KisAslWriterUtils::writeFixedString(!useLfxsLayerStyleFormat ? "lfx2" : "lfxs", io); KisAslWriterUtils::OffsetStreamPusher lfx2SizeTag(io, 2); try { KisAslWriter writer; writer.writePsdLfx2SectionEx(io, stylesXmlDoc); } catch (KisAslWriterUtils::ASLWriteException &e) { warnKrita << "WARNING: Couldn't save layer style lfx2 block:" << PREPEND_METHOD(e.what()); // TODO: make this error recoverable! throw e; } } void PsdAdditionalLayerInfoBlock::writePattBlockEx(QIODevice* io, const QDomDocument &patternsXmlDoc) { KisAslWriterUtils::writeFixedString("8BIM", io); KisAslWriterUtils::writeFixedString("Patt", io); KisAslWriterUtils::OffsetStreamPusher pattSizeTag(io, 2); try { KisAslPatternsWriter writer(patternsXmlDoc, io); writer.writePatterns(); } catch (KisAslWriterUtils::ASLWriteException &e) { warnKrita << "WARNING: Couldn't save layer style patterns block:" << PREPEND_METHOD(e.what()); // TODO: make this error recoverable! throw e; } } diff --git a/plugins/impex/psd/psd_additional_layer_info_block.h b/plugins/impex/psd/psd_additional_layer_info_block.h index 72a6374545..4dc020ac26 100644 --- a/plugins/impex/psd/psd_additional_layer_info_block.h +++ b/plugins/impex/psd/psd_additional_layer_info_block.h @@ -1,295 +1,295 @@ /* * Copyright (c) 2014 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef PSD_ADDITIONAL_LAYER_INFO_BLOCK_H #define PSD_ADDITIONAL_LAYER_INFO_BLOCK_H #include #include #include #include #include #include #include #include #include #include #include "psd.h" #include "psd_header.h" // additional layer information // LEVELS // Level record struct psd_layer_level_record { quint16 input_floor; // (0...253) quint16 input_ceiling; // (2...255) quint16 output_floor; // 255). Matched to input floor. quint16 output_ceiling; // (0...255) float gamma; // Short integer from 10...999 representing 0.1...9.99. Applied to all image data. }; // Levels settings files are loaded and saved in the Levels dialog. struct psd_layer_levels { psd_layer_level_record record[29]; // 29 sets of level records, each level containing 5 qint8 integers // Photoshop CS (8.0) Additional information // At the end of the Version 2 file is the following information: quint16 extra_level_count; // Count of total level record structures. Subtract the legacy number of level record structures, 29, to determine how many are remaining in the file for reading. psd_layer_level_record *extra_record; // Additianol level records according to count quint8 lookup_table[3][256]; }; // CURVES // The following is the data for each curve specified by count above struct psd_layer_curves_data { quint16 channel_index; // Before each curve is a channel index. quint16 point_count; // Count of points in the curve (qint8 integer from 2...19) quint16 output_value[19]; // All coordinates have range 0 to 255 quint16 input_value[19]; }; // Curves file format struct psd_layer_curves { quint16 curve_count; // Count of curves in the file. psd_layer_curves_data * curve; quint8 lookup_table[3][256]; }; // BRIGHTNESS AND CONTRAST struct psd_layer_brightness_contrast { qint8 brightness; qint8 contrast; qint8 mean_value; // for brightness and contrast qint8 Lab_color; quint8 lookup_table[256]; }; // COLOR BALANCE struct psd_layer_color_balance { qint8 cyan_red[3]; // (-100...100). shadows, midtones, highlights qint8 magenta_green[3]; qint8 yellow_blue[3]; bool preserve_luminosity; quint8 lookup_table[3][256]; }; // HUE/SATURATION // Hue/Saturation settings files are loaded and saved in Photoshop¡¯s Hue/Saturation dialog struct psd_layer_hue_saturation { quint8 hue_or_colorization; // 0 = Use settings for hue-adjustment; 1 = Use settings for colorization. qint8 colorization_hue; // Photoshop 5.0: The actual values are stored for the new version. Hue is - 180...180, Saturation is 0...100, and Lightness is -100...100. qint8 colorization_saturation;// Photoshop 4.0: Three qint8 integers Hue, Saturation, and Lightness from ¨C100...100. qint8 colorization_lightness; // The user interface represents hue as ¨C180...180, saturation as 0...100, and Lightness as -100...1000, as the traditional HSB color wheel, with red = 0. qint8 master_hue; // Master hue, saturation and lightness values. qint8 master_saturation; qint8 master_lightness; qint8 range_values[6][4]; // For RGB and CMYK, those values apply to each of the six hextants in the HSB color wheel: those image pixels nearest to red, yellow, green, cyan, blue, or magenta. These numbers appear in the user interface from ¨C60...60, however the slider will reflect each of the possible 201 values from ¨C100...100. qint8 setting_values[6][3]; // For Lab, the first four of the six values are applied to image pixels in the four Lab color quadrants, yellow, green, blue, and magenta. The other two values are ignored ( = 0). The values appear in the user interface from ¨C90 to 90. quint8 lookup_table[6][360]; }; // SELECTIVE COLOR // Selective Color settings files are loaded and saved in Photoshop¡¯s Selective Color dialog. struct psd_layer_selective_color { quint16 correction_method; // 0 = Apply color correction in relative mode; 1 = Apply color correction in absolute mode. qint8 cyan_correction[10]; // Amount of cyan correction. Short integer from ¨C100...100. qint8 magenta_correction[10]; // Amount of magenta correction. Short integer from ¨C100...100. qint8 yellow_correction[10]; // Amount of yellow correction. Short integer from ¨C100...100. qint8 black_correction[10]; // Amount of black correction. Short integer from ¨C100...100. }; // THRESHOLD struct psd_layer_threshold { quint16 level; // (1...255) } ; // INVERT // no parameter // POSTERIZE struct psd_layer_posterize { quint16 levels; // (2...255) quint8 lookup_table[256]; }; // CHANNEL MIXER struct psd_layer_channel_mixer { bool monochrome; qint8 red_cyan[4]; // RGB or CMYK color plus constant for the mixer settings. 4 * 2 bytes of color with 2 bytes of constant. qint8 green_magenta[4]; // (-200...200) qint8 blue_yellow[4]; qint8 black[4]; qint8 constant[4]; }; // PHOTO FILTER struct psd_layer_photo_filter { qint32 x_color; // 4 bytes each for XYZ color qint32 y_color; qint32 z_color; qint32 density; // (1...100) bool preserve_luminosity; }; #include struct psd_layer_solid_color { quint32 id; QColor fill_color; }; struct psd_layer_gradient_fill { quint32 id; double angle; psd_gradient_style style; qint32 scale; bool reverse; // Is gradient reverse bool dithered; // Is gradient dithered bool align_with_layer; psd_gradient_color gradient_color; }; struct psd_layer_pattern_fill { quint32 id; psd_pattern_info pattern_info; qint32 scale; }; struct psd_layer_type_face { qint8 mark; // Mark value qint32 font_type; // Font type data qint8 font_name[256]; // Pascal string of font name qint8 font_family_name[256]; // Pascal string of font family name qint8 font_style_name[256]; // Pascal string of font style name qint8 script; // Script value qint32 number_axes_vector; // Number of design axes vector to follow qint32 * vector; // Design vector value }; struct psd_layer_type_style { qint8 mark; // Mark value qint8 face_mark; // Face mark value qint32 size; // Size value qint32 tracking; // Tracking value qint32 kerning; // Kerning value qint32 leading; // Leading value qint32 base_shift; // Base shift value bool auto_kern; // Auto kern on/off bool rotate; // Rotate up/down }; struct psd_layer_type_line { qint32 char_count; // Character count value qint8 orientation; // Orientation value qint8 alignment; // Alignment value qint8 actual_char; // Actual character as a double byte character qint8 style; // Style value }; struct psd_layer_type_tool { double transform_info[6]; // 6 * 8 double precision numbers for the transform information qint8 faces_count; // Count of faces psd_layer_type_face * face; qint8 styles_count; // Count of styles psd_layer_type_style * style; qint8 type; // Type value qint32 scaling_factor; // Scaling factor value qint32 character_count; // Character count value qint32 horz_place; // Horizontal placement qint32 vert_place; // Vertical placement qint32 select_start; // Select start value qint32 select_end; // Select end value qint8 lines_count; // Line count psd_layer_type_line * line; QColor color; bool anti_alias; // Anti alias on/off }; /** * @brief The PsdAdditionalLayerInfoBlock class implements the Additional Layer Information block * * See: http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_71546 */ class PsdAdditionalLayerInfoBlock { public: PsdAdditionalLayerInfoBlock(const PSDHeader& header); typedef boost::function ExtraLayerInfoBlockHandler; void setExtraLayerInfoBlockHandler(ExtraLayerInfoBlockHandler handler); bool read(QIODevice* io); bool write(QIODevice* io, KisNodeSP node); void writeLuniBlockEx(QIODevice* io, const QString &layerName); void writeLsctBlockEx(QIODevice* io, psd_section_type sectionType, bool isPassThrough, const QString &blendModeKey); - void writeLfx2BlockEx(QIODevice* io, const QDomDocument &stylesXmlDoc); + void writeLfx2BlockEx(QIODevice* io, const QDomDocument &stylesXmlDoc, bool useLfxsLayerStyleFormat); void writePattBlockEx(QIODevice* io, const QDomDocument &patternsXmlDoc); bool valid(); const PSDHeader &m_header; QString error; QStringList keys; // List of all the keys that we've seen QString unicodeLayerName; QDomDocument layerStyleXml; QVector embeddedPatterns; psd_section_type sectionDividerType; QString sectionDividerBlendMode; private: void readImpl(QIODevice* io); private: ExtraLayerInfoBlockHandler m_layerInfoBlockHandler; }; #endif // PSD_ADDITIONAL_LAYER_INFO_BLOCK_H diff --git a/plugins/impex/psd/psd_layer_record.cpp b/plugins/impex/psd/psd_layer_record.cpp index 594d87de9b..314355fad6 100644 --- a/plugins/impex/psd/psd_layer_record.cpp +++ b/plugins/impex/psd/psd_layer_record.cpp @@ -1,759 +1,760 @@ /* * Copyright (c) 2009 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "psd_layer_record.h" #include #include #include #include #include #include #include #include #include "kis_iterator_ng.h" #include #include "psd_utils.h" #include "psd_header.h" #include "compression.h" #include #include #include #include #include #include #include #include "psd_pixel_utils.h" #include // Just for pretty debug messages QString channelIdToChannelType(int channelId, psd_color_mode colormode) { switch(channelId) { case -3: return "Real User Supplied Layer Mask (when both a user mask and a vector mask are present"; case -2: return "User Supplied Layer Mask"; case -1: return "Transparency mask"; case 0: switch(colormode) { case Bitmap: case Indexed: return QString("bitmap or indexed: %1").arg(channelId); case Grayscale: case Gray16: return "gray"; case RGB: case RGB48: return "red"; case Lab: case Lab48: return "L"; case CMYK: case CMYK64: return "cyan"; case MultiChannel: case DeepMultichannel: return QString("multichannel channel %1").arg(channelId); case DuoTone: case Duotone16: return QString("duotone channel %1").arg(channelId); default: return QString("unknown: %1").arg(channelId); }; case 1: switch(colormode) { case Bitmap: case Indexed: return QString("WARNING bitmap or indexed: %1").arg(channelId); case Grayscale: case Gray16: return QString("WARNING: %1").arg(channelId); case RGB: case RGB48: return "green"; case Lab: case Lab48: return "a"; case CMYK: case CMYK64: return "Magenta"; case MultiChannel: case DeepMultichannel: return QString("multichannel channel %1").arg(channelId); case DuoTone: case Duotone16: return QString("duotone channel %1").arg(channelId); default: return QString("unknown: %1").arg(channelId); }; case 2: switch(colormode) { case Bitmap: case Indexed: return QString("WARNING bitmap or indexed: %1").arg(channelId); case Grayscale: case Gray16: return QString("WARNING: %1").arg(channelId); case RGB: case RGB48: return "blue"; case Lab: case Lab48: return "b"; case CMYK: case CMYK64: return "yellow"; case MultiChannel: case DeepMultichannel: return QString("multichannel channel %1").arg(channelId); case DuoTone: case Duotone16: return QString("duotone channel %1").arg(channelId); default: return QString("unknown: %1").arg(channelId); }; case 3: switch(colormode) { case Bitmap: case Indexed: return QString("WARNING bitmap or indexed: %1").arg(channelId); case Grayscale: case Gray16: return QString("WARNING: %1").arg(channelId); case RGB: case RGB48: return QString("alpha: %1").arg(channelId); case Lab: case Lab48: return QString("alpha: %1").arg(channelId); case CMYK: case CMYK64: return "Key"; case MultiChannel: case DeepMultichannel: return QString("multichannel channel %1").arg(channelId); case DuoTone: case Duotone16: return QString("duotone channel %1").arg(channelId); default: return QString("unknown: %1").arg(channelId); }; default: return QString("unknown: %1").arg(channelId); }; } PSDLayerRecord::PSDLayerRecord(const PSDHeader& header) : top(0) , left(0) , bottom(0) , right(0) , nChannels(0) , opacity(0) , clipping(0) , transparencyProtected(false) , visible(true) , irrelevant(false) , layerName("UNINITIALIZED") , infoBlocks(header) , m_transparencyMaskSizeOffset(0) , m_header(header) { } bool PSDLayerRecord::read(QIODevice* io) { dbgFile << "Going to read layer record. Pos:" << io->pos(); if (!psdread(io, &top) || !psdread(io, &left) || !psdread(io, &bottom) || !psdread(io, &right) || !psdread(io, &nChannels)) { error = "could not read layer record"; return false; } dbgFile << "\ttop" << top << "left" << left << "bottom" << bottom << "right" << right << "number of channels" << nChannels; Q_ASSERT(top <= bottom); Q_ASSERT(left <= right); Q_ASSERT(nChannels > 0); if (nChannels < 1) { error = QString("Not enough channels. Got: %1").arg(nChannels); return false; } if (nChannels > MAX_CHANNELS) { error = QString("Too many channels. Got: %1").arg(nChannels); return false; } for (int i = 0; i < nChannels; ++i) { if (io->atEnd()) { error = "Could not read enough data for channels"; return false; } ChannelInfo* info = new ChannelInfo; if (!psdread(io, &info->channelId)) { error = "could not read channel id"; delete info; return false; } bool r; if (m_header.version == 1) { quint32 channelDataLength; r = psdread(io, &channelDataLength); info->channelDataLength = (quint64)channelDataLength; } else { r = psdread(io, &info->channelDataLength); } if (!r) { error = "Could not read length for channel data"; delete info; return false; } dbgFile << "\tchannel" << i << "id" << channelIdToChannelType(info->channelId, m_header.colormode) << "length" << info->channelDataLength << "start" << info->channelDataStart << "offset" << info->channelOffset << "channelInfoPosition" << info->channelInfoPosition; channelInfoRecords << info; } if (!psd_read_blendmode(io, blendModeKey)) { error = QString("Could not read blend mode key. Got: %1").arg(blendModeKey); return false; } dbgFile << "\tBlend mode" << blendModeKey << "pos" << io->pos(); if (!psdread(io, &opacity)) { error = "Could not read opacity"; return false; } dbgFile << "\tOpacity" << opacity << io->pos(); if (!psdread(io, &clipping)) { error = "Could not read clipping"; return false; } dbgFile << "\tclipping" << clipping << io->pos(); quint8 flags; if (!psdread(io, &flags)) { error = "Could not read flags"; return false; } dbgFile << "\tflags" << flags << io->pos(); transparencyProtected = flags & 1 ? true : false; dbgFile << "\ttransparency protected" << transparencyProtected; visible = flags & 2 ? false : true; dbgFile << "\tvisible" << visible; if (flags & 8) { irrelevant = flags & 16 ? true : false; } else { irrelevant = false; } dbgFile << "\tirrelevant" << irrelevant; dbgFile << "\tfiller at " << io->pos(); quint8 filler; if (!psdread(io, &filler) || filler != 0) { error = "Could not read padding"; return false; } dbgFile << "\tGoing to read extra data length" << io->pos(); quint32 extraDataLength; if (!psdread(io, &extraDataLength) || io->bytesAvailable() < extraDataLength) { error = QString("Could not read extra layer data: %1 at pos %2").arg(extraDataLength).arg(io->pos()); return false; } dbgFile << "\tExtra data length" << extraDataLength; if (extraDataLength > 0) { dbgFile << "Going to read extra data field. Bytes available: " << io->bytesAvailable() << "pos" << io->pos(); quint32 layerMaskLength = 1; // invalid... if (!psdread(io, &layerMaskLength) || io->bytesAvailable() < layerMaskLength || !(layerMaskLength == 0 || layerMaskLength == 20 || layerMaskLength == 36)) { error = QString("Could not read layer mask length: %1").arg(layerMaskLength); return false; } memset(&layerMask, 0, sizeof(LayerMaskData)); if (layerMaskLength == 20 || layerMaskLength == 36) { if (!psdread(io, &layerMask.top) || !psdread(io, &layerMask.left) || !psdread(io, &layerMask.bottom) || !psdread(io, &layerMask.right) || !psdread(io, &layerMask.defaultColor) || !psdread(io, &flags)) { error = "could not read mask record"; return false; } } if (layerMaskLength == 20) { quint16 padding; if (!psdread(io, &padding)) { error = "Could not read layer mask padding"; return false; } } if (layerMaskLength == 36 ) { if (!psdread(io, &flags) || !psdread(io, &layerMask.defaultColor) || !psdread(io, &layerMask.top) || !psdread(io, &layerMask.left) || !psdread(io, &layerMask.bottom) || !psdread(io, &layerMask.top)) { error = "could not read 'real' mask record"; return false; } } layerMask.positionedRelativeToLayer = flags & 1 ? true : false; layerMask.disabled = flags & 2 ? true : false; layerMask.invertLayerMaskWhenBlending = flags & 4 ? true : false; dbgFile << "\tRead layer mask/adjustment layer data. Length of block:" << layerMaskLength << "pos" << io->pos(); // layer blending thingies quint32 blendingDataLength; if (!psdread(io, &blendingDataLength) || io->bytesAvailable() < blendingDataLength) { error = "Could not read extra blending data."; return false; } //dbgFile << "blending block data length" << blendingDataLength << ", pos" << io->pos(); blendingRanges.data = io->read(blendingDataLength); if ((quint32)blendingRanges.data.size() != blendingDataLength) { error = QString("Got %1 bytes for the blending range block, needed %2").arg(blendingRanges.data.size(), blendingDataLength); } /* // XXX: reading this block correctly failed, I have more channel ranges than I'd expected. if (!psdread(io, &blendingRanges.blackValues[0]) || !psdread(io, &blendingRanges.blackValues[1]) || !psdread(io, &blendingRanges.whiteValues[0]) || !psdread(io, &blendingRanges.whiteValues[1]) || !psdread(io, &blendingRanges.compositeGrayBlendDestinationRange)) { error = "Could not read blending black/white values"; return false; } for (int i = 0; i < nChannels; ++i) { quint32 src; quint32 dst; if (!psdread(io, &src) || !psdread(io, &dst)) { error = QString("could not read src/dst range for channel %1").arg(i); return false; } dbgFile << "\tread range " << src << "to" << dst << "for channel" << i; blendingRanges.sourceDestinationRanges << QPair(src, dst); } */ dbgFile << "\tGoing to read layer name at" << io->pos(); quint8 layerNameLength; if (!psdread(io, &layerNameLength)) { error = "Could not read layer name length"; return false; } dbgFile << "\tlayer name length unpadded" << layerNameLength << "pos" << io->pos(); layerNameLength = ((layerNameLength + 1 + 3) & ~0x03) - 1; dbgFile << "\tlayer name length padded" << layerNameLength << "pos" << io->pos(); layerName = io->read(layerNameLength); dbgFile << "\tlayer name" << layerName << io->pos(); if (!infoBlocks.read(io)) { error = infoBlocks.error; return false; } if (infoBlocks.keys.contains("luni") && !infoBlocks.unicodeLayerName.isEmpty()) { layerName = infoBlocks.unicodeLayerName; } } return valid(); } void PSDLayerRecord::write(QIODevice* io, KisPaintDeviceSP layerContentDevice, KisNodeSP onlyTransparencyMask, const QRect &maskRect, psd_section_type sectionType, - const QDomDocument &stylesXmlDoc) + const QDomDocument &stylesXmlDoc, + bool useLfxsLayerStyleFormat) { dbgFile << "writing layer info record" << "at" << io->pos(); m_layerContentDevice = layerContentDevice; m_onlyTransparencyMask = onlyTransparencyMask; m_onlyTransparencyMaskRect = maskRect; dbgFile << "saving layer record for " << layerName << "at pos" << io->pos(); dbgFile << "\ttop" << top << "left" << left << "bottom" << bottom << "right" << right << "number of channels" << nChannels; Q_ASSERT(left <= right); Q_ASSERT(top <= bottom); Q_ASSERT(nChannels > 0); try { const QRect layerRect(left, top, right - left, bottom - top); KisAslWriterUtils::writeRect(layerRect, io); { quint16 realNumberOfChannels = nChannels + bool(m_onlyTransparencyMask); SAFE_WRITE_EX(io, realNumberOfChannels); } Q_FOREACH (ChannelInfo *channel, channelInfoRecords) { SAFE_WRITE_EX(io, (quint16)channel->channelId); channel->channelInfoPosition = io->pos(); // to be filled in when we know how big channel block is const quint32 fakeChannelSize = 0; SAFE_WRITE_EX(io, fakeChannelSize); } if (m_onlyTransparencyMask) { const quint16 userSuppliedMaskChannelId = -2; SAFE_WRITE_EX(io, userSuppliedMaskChannelId); m_transparencyMaskSizeOffset = io->pos(); const quint32 fakeTransparencyMaskSize = 0; SAFE_WRITE_EX(io, fakeTransparencyMaskSize); } // blend mode dbgFile << ppVar(blendModeKey) << ppVar(io->pos()); KisAslWriterUtils::writeFixedString("8BIM", io); KisAslWriterUtils::writeFixedString(blendModeKey, io); SAFE_WRITE_EX(io, opacity); SAFE_WRITE_EX(io, clipping); // unused // visibility and protection quint8 flags = 0; if (transparencyProtected) flags |= 1; if (!visible) flags |= 2; if (irrelevant) { flags |= (1 << 3) | (1 << 4); } SAFE_WRITE_EX(io, flags); { quint8 padding = 0; SAFE_WRITE_EX(io, padding); } { // extra fields with their own length tag KisAslWriterUtils::OffsetStreamPusher extraDataSizeTag(io); if (m_onlyTransparencyMask) { { const quint32 layerMaskDataSize = 20; // support simple case only SAFE_WRITE_EX(io, layerMaskDataSize); } KisAslWriterUtils::writeRect(m_onlyTransparencyMaskRect, io); { // NOTE: in PSD the default color of the mask is stored in 1 byte value! // Even when the mask is actually 16/32 bit! I have no idea how it is // actually treated in this case. KIS_ASSERT_RECOVER_NOOP(m_onlyTransparencyMask->paintDevice()->pixelSize() == 1); const quint8 defaultPixel = *m_onlyTransparencyMask->paintDevice()->defaultPixel().data(); SAFE_WRITE_EX(io, defaultPixel); } { const quint8 maskFlags = 0; // nothing serious SAFE_WRITE_EX(io, maskFlags); const quint16 padding = 0; // 2-byte padding SAFE_WRITE_EX(io, padding); } } else { const quint32 nullLayerMaskDataSize = 0; SAFE_WRITE_EX(io, nullLayerMaskDataSize); } { // blending ranges are not implemented yet const quint32 nullBlendingRangesSize = 0; SAFE_WRITE_EX(io, nullBlendingRangesSize); } // layer name: Pascal string, padded to a multiple of 4 bytes. psdwrite_pascalstring(io, layerName, 4); PsdAdditionalLayerInfoBlock additionalInfoBlock(m_header); // write 'luni' data block additionalInfoBlock.writeLuniBlockEx(io, layerName); // write 'lsct' data block if (sectionType != psd_other) { additionalInfoBlock.writeLsctBlockEx(io, sectionType, isPassThrough, blendModeKey); } // write 'lfx2' data block if (!stylesXmlDoc.isNull()) { - additionalInfoBlock.writeLfx2BlockEx(io, stylesXmlDoc); + additionalInfoBlock.writeLfx2BlockEx(io, stylesXmlDoc, useLfxsLayerStyleFormat); } } } catch (KisAslWriterUtils::ASLWriteException &e) { throw KisAslWriterUtils::ASLWriteException(PREPEND_METHOD(e.what())); } } KisPaintDeviceSP PSDLayerRecord::convertMaskDeviceIfNeeded(KisPaintDeviceSP dev) { KisPaintDeviceSP result = dev; if (m_header.channelDepth == 16) { result = new KisPaintDevice(*dev); delete result->convertTo(KoColorSpaceRegistry::instance()->alpha16()); } else if (m_header.channelDepth == 32) { result = new KisPaintDevice(*dev); delete result->convertTo(KoColorSpaceRegistry::instance()->alpha32f()); } return result; } void PSDLayerRecord::writeTransparencyMaskPixelData(QIODevice *io) { if (m_onlyTransparencyMask) { KisPaintDeviceSP device = convertMaskDeviceIfNeeded(m_onlyTransparencyMask->paintDevice()); QByteArray buffer(device->pixelSize() * m_onlyTransparencyMaskRect.width() * m_onlyTransparencyMaskRect.height(), 0); device->readBytes((quint8*)buffer.data(), m_onlyTransparencyMaskRect); PsdPixelUtils::writeChannelDataRLE(io, (quint8*)buffer.data(), device->pixelSize(), m_onlyTransparencyMaskRect, m_transparencyMaskSizeOffset, -1, true); } } void PSDLayerRecord::writePixelData(QIODevice *io) { try { writePixelDataImpl(io); } catch (KisAslWriterUtils::ASLWriteException &e) { throw KisAslWriterUtils::ASLWriteException(PREPEND_METHOD(e.what())); } } void PSDLayerRecord::writePixelDataImpl(QIODevice *io) { dbgFile << "writing pixel data for layer" << layerName << "at" << io->pos(); KisPaintDeviceSP dev = m_layerContentDevice; const QRect rc(left, top, right - left, bottom - top); if (rc.isEmpty()) { dbgFile << "Layer is empty! Writing placeholder information."; for (int i = 0; i < nChannels; i++) { const ChannelInfo *channelInfo = channelInfoRecords[i]; KisAslWriterUtils::OffsetStreamPusher channelBlockSizeExternalTag(io, 0, channelInfo->channelInfoPosition); SAFE_WRITE_EX(io, (quint16)Compression::Uncompressed); } writeTransparencyMaskPixelData(io); return; } // now write all the channels in display order dbgFile << "layer" << layerName; const int channelSize = m_header.channelDepth / 8; const psd_color_mode colorMode = m_header.colormode; QVector writingInfoList; Q_FOREACH (const ChannelInfo *channelInfo, channelInfoRecords) { writingInfoList << PsdPixelUtils::ChannelWritingInfo(channelInfo->channelId, channelInfo->channelInfoPosition); } PsdPixelUtils::writePixelDataCommon(io, dev, rc, colorMode, channelSize, true, true, writingInfoList); writeTransparencyMaskPixelData(io); } bool PSDLayerRecord::valid() { // XXX: check validity! return true; } bool PSDLayerRecord::readPixelData(QIODevice *io, KisPaintDeviceSP device) { dbgFile << "Reading pixel data for layer" << layerName << "pos" << io->pos(); const int channelSize = m_header.channelDepth / 8; const QRect layerRect = QRect(left, top, right - left, bottom - top); try { PsdPixelUtils::readChannels(io, device, m_header.colormode, channelSize, layerRect, channelInfoRecords); } catch (KisAslReaderUtils::ASLParseException &e) { device->clear(); error = e.what(); return false; } return true; } QRect PSDLayerRecord::channelRect(ChannelInfo *channel) const { QRect result; if (channel->channelId < -1) { result = QRect(layerMask.left, layerMask.top, layerMask.right - layerMask.left, layerMask.bottom - layerMask.top); } else { result = QRect(left, top, right - left, bottom - top); } return result; } bool PSDLayerRecord::readMask(QIODevice *io, KisPaintDeviceSP dev, ChannelInfo *channelInfo) { KIS_ASSERT_RECOVER(channelInfo->channelId < -1) { return false; } dbgFile << "Going to read" << channelIdToChannelType(channelInfo->channelId, m_header.colormode) << "mask"; QRect maskRect = channelRect(channelInfo); if (maskRect.isEmpty()) { dbgFile << "Empty Channel"; return true; } // the device must be a pixel selection KIS_ASSERT_RECOVER(dev->pixelSize() == 1) { return false; } dev->setDefaultPixel(KoColor(&layerMask.defaultColor, dev->colorSpace())); const int pixelSize = m_header.channelDepth == 16 ? 2 : m_header.channelDepth == 32 ? 4 : 1; QVector infoRecords; infoRecords << channelInfo; PsdPixelUtils::readAlphaMaskChannels(io, dev, pixelSize, maskRect, infoRecords); return true; } QDebug operator<<(QDebug dbg, const PSDLayerRecord &layer) { #ifndef NODEBUG dbg.nospace() << "valid: " << const_cast(&layer)->valid(); dbg.nospace() << ", name: " << layer.layerName; dbg.nospace() << ", top: " << layer.top; dbg.nospace() << ", left:" << layer.left; dbg.nospace() << ", bottom: " << layer.bottom; dbg.nospace() << ", right: " << layer.right; dbg.nospace() << ", number of channels: " << layer.nChannels; dbg.nospace() << ", blendModeKey: " << layer.blendModeKey; dbg.nospace() << ", opacity: " << layer.opacity; dbg.nospace() << ", clipping: " << layer.clipping; dbg.nospace() << ", transparency protected: " << layer.transparencyProtected; dbg.nospace() << ", visible: " << layer.visible; dbg.nospace() << ", irrelevant: " << layer.irrelevant << "\n"; Q_FOREACH (const ChannelInfo* channel, layer.channelInfoRecords) { dbg.space() << channel; } #endif return dbg.nospace(); } QDebug operator<<(QDebug dbg, const ChannelInfo &channel) { #ifndef NODEBUG dbg.nospace() << "\tChannel type" << channel.channelId << "size: " << channel.channelDataLength << "compression type" << channel.compressionType << "\n"; #endif return dbg.nospace(); } diff --git a/plugins/impex/psd/psd_layer_record.h b/plugins/impex/psd/psd_layer_record.h index 002411986b..3ccb03864c 100644 --- a/plugins/impex/psd/psd_layer_record.h +++ b/plugins/impex/psd/psd_layer_record.h @@ -1,178 +1,178 @@ /* * Copyright (c) 2009 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef PSD_LAYER_RECORD_H #define PSD_LAYER_RECORD_H #include #include #include #include #include #include #include #include "psd.h" #include "psd_header.h" #include "compression.h" #include "psd_additional_layer_info_block.h" #include class QIODevice; enum psd_layer_type { psd_layer_type_normal, psd_layer_type_hidden, psd_layer_type_folder, psd_layer_type_solid_color, psd_layer_type_gradient_fill, psd_layer_type_pattern_fill, psd_layer_type_levels, psd_layer_type_curves, psd_layer_type_brightness_contrast, psd_layer_type_color_balance, psd_layer_type_hue_saturation, psd_layer_type_selective_color, psd_layer_type_threshold, psd_layer_type_invert, psd_layer_type_posterize, psd_layer_type_channel_mixer, psd_layer_type_gradient_map, psd_layer_type_photo_filter, }; struct ChannelInfo { ChannelInfo() : channelId(0) , compressionType(Compression::Unknown) , channelDataStart(0) , channelDataLength(0) , channelOffset(0) , channelInfoPosition(0) {} qint16 channelId; // 0 red, 1 green, 2 blue, -1 transparency, -2 user-supplied layer mask Compression::CompressionType compressionType; quint64 channelDataStart; quint64 channelDataLength; QVector rleRowLengths; int channelOffset; // where the channel data starts int channelInfoPosition; // where the channelinfo record is saved in the file }; class PSDLayerRecord { public: PSDLayerRecord(const PSDHeader &header); ~PSDLayerRecord() { qDeleteAll(channelInfoRecords); } QRect channelRect(ChannelInfo *channel) const; bool read(QIODevice* io); bool readPixelData(QIODevice* io, KisPaintDeviceSP device); bool readMask(QIODevice* io, KisPaintDeviceSP dev, ChannelInfo *channel); - void write(QIODevice* io, KisPaintDeviceSP layerContentDevice, KisNodeSP onlyTransparencyMask, const QRect &maskRect, psd_section_type sectionType, const QDomDocument &stylesXmlDoc); + void write(QIODevice* io, KisPaintDeviceSP layerContentDevice, KisNodeSP onlyTransparencyMask, const QRect &maskRect, psd_section_type sectionType, const QDomDocument &stylesXmlDoc, bool useLfxsLayerStyleFormat); void writePixelData(QIODevice* io); bool valid(); QString error; qint32 top; qint32 left; qint32 bottom; qint32 right; quint16 nChannels; QVector channelInfoRecords; QString blendModeKey; bool isPassThrough; quint8 opacity; quint8 clipping; bool transparencyProtected; bool visible; bool irrelevant; struct LayerMaskData { qint32 top; qint32 left; qint32 bottom; qint32 right; quint8 defaultColor; // 0 or 255 bool positionedRelativeToLayer; bool disabled; bool invertLayerMaskWhenBlending; quint8 userMaskDensity; double userMaskFeather; quint8 vectorMaskDensity; double vectorMaskFeather; }; LayerMaskData layerMask; struct LayerBlendingRanges { QByteArray data; quint8 blackValues[2]; quint8 whiteValues[2]; quint32 compositeGrayBlendDestinationRange; QVector > sourceDestinationRanges; }; LayerBlendingRanges blendingRanges; QString layerName; // pascal, not unicode! PsdAdditionalLayerInfoBlock infoBlocks; private: void writeTransparencyMaskPixelData(QIODevice *io); void writePixelDataImpl(QIODevice *io); KisPaintDeviceSP convertMaskDeviceIfNeeded(KisPaintDeviceSP dev); private: KisPaintDeviceSP m_layerContentDevice; KisNodeSP m_onlyTransparencyMask; QRect m_onlyTransparencyMaskRect; qint64 m_transparencyMaskSizeOffset; const PSDHeader m_header; }; QDebug operator<<(QDebug dbg, const PSDLayerRecord& layer); QDebug operator<<(QDebug dbg, const ChannelInfo& layer); #endif // PSD_LAYER_RECORD_H diff --git a/plugins/impex/psd/psd_layer_section.cpp b/plugins/impex/psd/psd_layer_section.cpp index a9efb0ac23..2c064a5bce 100644 --- a/plugins/impex/psd/psd_layer_section.cpp +++ b/plugins/impex/psd/psd_layer_section.cpp @@ -1,588 +1,589 @@ /* * Copyright (c) 2009 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "psd_layer_section.h" #include #include #include #include #include #include #include #include #include #include "kis_dom_utils.h" #include "psd_header.h" #include "psd_utils.h" #include "compression.h" #include #include #include #include PSDLayerMaskSection::PSDLayerMaskSection(const PSDHeader& header) : globalInfoSection(header), m_header(header) { hasTransparency = false; layerMaskBlockSize = 0; nLayers = 0; } PSDLayerMaskSection::~PSDLayerMaskSection() { qDeleteAll(layers); } bool PSDLayerMaskSection::read(QIODevice* io) { bool retval = true; // be optimistic! <:-) try { retval = readImpl(io); } catch (KisAslReaderUtils::ASLParseException &e) { warnKrita << "WARNING: PSD (emb. pattern):" << e.what(); retval = false; } return retval; } bool PSDLayerMaskSection::readLayerInfoImpl(QIODevice* io) { quint32 layerInfoSectionSize = 0; SAFE_READ_EX(io, layerInfoSectionSize); if (layerInfoSectionSize & 0x1) { warnKrita << "WARNING: layerInfoSectionSize is NOT even! Fixing..."; layerInfoSectionSize++; } { SETUP_OFFSET_VERIFIER(layerInfoSectionTag, io, layerInfoSectionSize, 0); dbgFile << "Layer info block size" << layerInfoSectionSize; if (layerInfoSectionSize > 0 ) { if (!psdread(io, &nLayers) || nLayers == 0) { error = QString("Could not read read number of layers or no layers in image. %1").arg(nLayers); return false; } hasTransparency = nLayers < 0; // first alpha channel is the alpha channel of the projection. nLayers = qAbs(nLayers); dbgFile << "Number of layers:" << nLayers; dbgFile << "Has separate projection transparency:" << hasTransparency; for (int i = 0; i < nLayers; ++i) { dbgFile << "Going to read layer" << i << "pos" << io->pos(); dbgFile << "== Enter PSDLayerRecord"; PSDLayerRecord *layerRecord = new PSDLayerRecord(m_header); if (!layerRecord->read(io)) { error = QString("Could not load layer %1: %2").arg(i).arg(layerRecord->error); return false; } dbgFile << "== Leave PSDLayerRecord"; dbgFile << "Finished reading layer" << i << layerRecord->layerName << "blending mode" << layerRecord->blendModeKey << io->pos() << "Number of channels:" << layerRecord->channelInfoRecords.size(); layers << layerRecord; } } // get the positions for the channels belonging to each layer for (int i = 0; i < nLayers; ++i) { dbgFile << "Going to seek channel positions for layer" << i << "pos" << io->pos(); if (i > layers.size()) { error = QString("Expected layer %1, but only have %2 layers").arg(i).arg(layers.size()); return false; } PSDLayerRecord *layerRecord = layers.at(i); for (int j = 0; j < layerRecord->nChannels; ++j) { // save the current location so we can jump beyond this block later on. quint64 channelStartPos = io->pos(); dbgFile << "\tReading channel image data for channel" << j << "from pos" << io->pos(); KIS_ASSERT_RECOVER(j < layerRecord->channelInfoRecords.size()) { return false; } ChannelInfo* channelInfo = layerRecord->channelInfoRecords.at(j); quint16 compressionType; if (!psdread(io, &compressionType)) { error = "Could not read compression type for channel"; return false; } channelInfo->compressionType = (Compression::CompressionType)compressionType; dbgFile << "\t\tChannel" << j << "has compression type" << compressionType; QRect channelRect = layerRecord->channelRect(channelInfo); // read the rle row lengths; if (channelInfo->compressionType == Compression::RLE) { for(qint64 row = 0; row < channelRect.height(); ++row) { //dbgFile << "Reading the RLE bytecount position of row" << row << "at pos" << io->pos(); quint32 byteCount; if (m_header.version == 1) { quint16 _byteCount; if (!psdread(io, &_byteCount)) { error = QString("Could not read byteCount for rle-encoded channel"); return 0; } byteCount = _byteCount; } else { if (!psdread(io, &byteCount)) { error = QString("Could not read byteCount for rle-encoded channel"); return 0; } } ////dbgFile << "rle byte count" << byteCount; channelInfo->rleRowLengths << byteCount; } } // we're beyond all the length bytes, rle bytes and whatever, this is the // location of the real pixel data channelInfo->channelDataStart = io->pos(); dbgFile << "\t\tstart" << channelStartPos << "data start" << channelInfo->channelDataStart << "data length" << channelInfo->channelDataLength << "pos" << io->pos(); // make sure we are at the start of the next channel data block io->seek(channelStartPos + channelInfo->channelDataLength); // this is the length of the actual channel data bytes channelInfo->channelDataLength = channelInfo->channelDataLength - (channelInfo->channelDataStart - channelStartPos); dbgFile << "\t\tchannel record" << j << "for layer" << i << "with id" << channelInfo->channelId << "starting position" << channelInfo->channelDataStart << "with length" << channelInfo->channelDataLength << "and has compression type" << channelInfo->compressionType; } } } return true; } bool PSDLayerMaskSection::readImpl(QIODevice* io) { dbgFile << "reading layer section. Pos:" << io->pos() << "bytes left:" << io->bytesAvailable(); layerMaskBlockSize = 0; if (m_header.version == 1) { quint32 _layerMaskBlockSize = 0; if (!psdread(io, &_layerMaskBlockSize) || _layerMaskBlockSize > (quint64)io->bytesAvailable()) { error = QString("Could not read layer + mask block size. Got %1. Bytes left %2") .arg(_layerMaskBlockSize).arg(io->bytesAvailable()); return false; } layerMaskBlockSize = _layerMaskBlockSize; } else if (m_header.version == 2) { if (!psdread(io, &layerMaskBlockSize) || layerMaskBlockSize > (quint64)io->bytesAvailable()) { error = QString("Could not read layer + mask block size. Got %1. Bytes left %2") .arg(layerMaskBlockSize).arg(io->bytesAvailable()); return false; } } quint64 start = io->pos(); dbgFile << "layer + mask section size" << layerMaskBlockSize; if (layerMaskBlockSize == 0) { dbgFile << "No layer + mask info, so no layers, only a background layer"; return true; } if (!readLayerInfoImpl(io)) { return false; } quint32 globalMaskBlockLength; if (!psdread(io, &globalMaskBlockLength)) { error = "Could not read global mask info block"; return false; } if (globalMaskBlockLength > 0) { if (!psdread(io, &globalLayerMaskInfo.overlayColorSpace)) { error = "Could not read global mask info overlay colorspace"; return false; } for (int i = 0; i < 4; ++i) { if (!psdread(io, &globalLayerMaskInfo.colorComponents[i])) { error = QString("Could not read mask info visualizaion color component %1").arg(i); return false; } } if (!psdread(io, &globalLayerMaskInfo.opacity)) { error = "Could not read global mask info visualization opacity"; return false; } if (!psdread(io, &globalLayerMaskInfo.kind)) { error = "Could not read global mask info visualization type"; return false; } } // global additional sections /** * Newer versions of PSD have layers info block wrapped into * 'Lr16' or 'Lr32' additional section, while the main block is * absent. * * Here we pass the callback which should be used when such * additional section is recognized. */ globalInfoSection.setExtraLayerInfoBlockHandler(std::bind(&PSDLayerMaskSection::readLayerInfoImpl, this, std::placeholders::_1)); globalInfoSection.read(io); /* put us after this section so reading the next section will work even if we mess up */ io->seek(start + layerMaskBlockSize); return true; } struct FlattenedNode { FlattenedNode() : type(RASTER_LAYER) {} KisNodeSP node; enum Type { RASTER_LAYER, FOLDER_OPEN, FOLDER_CLOSED, SECTION_DIVIDER }; Type type; }; void addBackgroundIfNeeded(KisNodeSP root, QList &nodes) { KisGroupLayer *group = dynamic_cast(root.data()); if (!group) return; KoColor projectionColor = group->defaultProjectionColor(); if (projectionColor.opacityU8() == OPACITY_TRANSPARENT_U8) return; KisPaintLayerSP layer = new KisPaintLayer(group->image(), i18nc("Automatically created layer name when saving into PSD", "Background"), OPACITY_OPAQUE_U8); layer->paintDevice()->setDefaultPixel(projectionColor); { FlattenedNode item; item.node = layer; item.type = FlattenedNode::RASTER_LAYER; nodes << item; } } void flattenNodes(KisNodeSP node, QList &nodes) { KisNodeSP child = node->firstChild(); while (child) { bool isGroupLayer = child->inherits("KisGroupLayer"); bool isRasterLayer = child->inherits("KisPaintLayer") || child->inherits("KisShapeLayer"); if (isGroupLayer) { { FlattenedNode item; item.node = child; item.type = FlattenedNode::SECTION_DIVIDER; nodes << item; } flattenNodes(child, nodes); { FlattenedNode item; item.node = child; item.type = FlattenedNode::FOLDER_OPEN; nodes << item; } } else if (isRasterLayer) { FlattenedNode item; item.node = child; item.type = FlattenedNode::RASTER_LAYER; nodes << item; } child = child->nextSibling(); } } KisNodeSP findOnlyTransparencyMask(KisNodeSP node, FlattenedNode::Type type) { if (type != FlattenedNode::FOLDER_OPEN && type != FlattenedNode::FOLDER_CLOSED && type != FlattenedNode::RASTER_LAYER) { return 0; } KisLayer *layer = qobject_cast(node.data()); QList masks = layer->effectMasks(); if (masks.size() != 1) return 0; KisEffectMaskSP onlyMask = masks.first(); return onlyMask->inherits("KisTransparencyMask") ? onlyMask : 0; } QDomDocument fetchLayerStyleXmlData(KisNodeSP node) { const KisLayer *layer = qobject_cast(node.data()); KisPSDLayerStyleSP layerStyle = layer->layerStyle(); if (!layerStyle) return QDomDocument(); KisAslLayerStyleSerializer serializer; serializer.setStyles(QVector() << layerStyle); return serializer.formPsdXmlDocument(); } inline QDomNode findNodeByKey(const QString &key, QDomNode parent) { return KisDomUtils::findElementByAttibute(parent, "node", "key", key); } void mergePatternsXMLSection(const QDomDocument &src, QDomDocument &dst) { QDomNode srcPatternsNode = findNodeByKey("Patterns", src.documentElement()); QDomNode dstPatternsNode = findNodeByKey("Patterns", dst.documentElement()); if (srcPatternsNode.isNull()) return; if (dstPatternsNode.isNull()) { dst = src; return; } KIS_ASSERT_RECOVER_RETURN(!srcPatternsNode.isNull()); KIS_ASSERT_RECOVER_RETURN(!dstPatternsNode.isNull()); QDomNode node = srcPatternsNode.firstChild(); while(!node.isNull()) { QDomNode importedNode = dst.importNode(node, true); KIS_ASSERT_RECOVER_RETURN(!importedNode.isNull()); dstPatternsNode.appendChild(importedNode); node = node.nextSibling(); } } bool PSDLayerMaskSection::write(QIODevice* io, KisNodeSP rootLayer) { bool retval = true; try { writeImpl(io, rootLayer); } catch (KisAslWriterUtils::ASLWriteException &e) { error = PREPEND_METHOD(e.what()); retval = false; } return retval; } void PSDLayerMaskSection::writeImpl(QIODevice* io, KisNodeSP rootLayer) { dbgFile << "Writing layer layer section"; // Build the whole layer structure QList nodes; addBackgroundIfNeeded(rootLayer, nodes); flattenNodes(rootLayer, nodes); if (nodes.isEmpty()) { throw KisAslWriterUtils::ASLWriteException("Could not find paint layers to save"); } { KisAslWriterUtils::OffsetStreamPusher layerAndMaskSectionSizeTag(io, 2); QDomDocument mergedPatternsXmlDoc; { KisAslWriterUtils::OffsetStreamPusher layerInfoSizeTag(io, 4); { // number of layers (negative, because krita always has alpha) const qint16 layersSize = -nodes.size(); SAFE_WRITE_EX(io, layersSize); dbgFile << "Number of layers" << layersSize << "at" << io->pos(); } // Layer records section Q_FOREACH (const FlattenedNode &item, nodes) { KisNodeSP node = item.node; PSDLayerRecord *layerRecord = new PSDLayerRecord(m_header); layers.append(layerRecord); KisNodeSP onlyTransparencyMask = findOnlyTransparencyMask(node, item.type); const QRect maskRect = onlyTransparencyMask ? onlyTransparencyMask->paintDevice()->exactBounds() : QRect(); const bool nodeVisible = node->visible(); const KoColorSpace *colorSpace = node->colorSpace(); const quint8 nodeOpacity = node->opacity(); const quint8 nodeClipping = 0; const KisPaintLayer *paintLayer = qobject_cast(node.data()); const bool alphaLocked = (paintLayer && paintLayer->alphaLocked()); const QString nodeCompositeOp = node->compositeOpId(); const KisGroupLayer *groupLayer = qobject_cast(node.data()); const bool nodeIsPassThrough = groupLayer && groupLayer->passThroughMode(); QDomDocument stylesXmlDoc = fetchLayerStyleXmlData(node); if (mergedPatternsXmlDoc.isNull() && !stylesXmlDoc.isNull()) { mergedPatternsXmlDoc = stylesXmlDoc; } else if (!mergedPatternsXmlDoc.isNull() && !stylesXmlDoc.isNull()) { mergePatternsXMLSection(stylesXmlDoc, mergedPatternsXmlDoc); } bool nodeIrrelevant = false; QString nodeName; KisPaintDeviceSP layerContentDevice; psd_section_type sectionType; if (item.type == FlattenedNode::RASTER_LAYER) { nodeIrrelevant = false; nodeName = node->name(); layerContentDevice = onlyTransparencyMask ? node->original() : node->projection(); sectionType = psd_other; } else { nodeIrrelevant = true; nodeName = item.type == FlattenedNode::SECTION_DIVIDER ? QString("") : node->name(); layerContentDevice = 0; sectionType = item.type == FlattenedNode::SECTION_DIVIDER ? psd_bounding_divider : item.type == FlattenedNode::FOLDER_OPEN ? psd_open_folder : psd_closed_folder; } // === no access to node anymore QRect layerRect; if (layerContentDevice) { QRect rc = layerContentDevice->exactBounds(); rc = rc.normalized(); // keep to the max of photoshop's capabilities if (rc.width() > 30000) rc.setWidth(30000); if (rc.height() > 30000) rc.setHeight(30000); layerRect = rc; } layerRecord->top = layerRect.y(); layerRecord->left = layerRect.x(); layerRecord->bottom = layerRect.y() + layerRect.height(); layerRecord->right = layerRect.x() + layerRect.width(); // colors + alpha channel // note: transparency mask not included layerRecord->nChannels = colorSpace->colorChannelCount() + 1; ChannelInfo *info = new ChannelInfo; info->channelId = -1; // For the alpha channel, which we always have in Krita, and should be saved first in layerRecord->channelInfoRecords << info; // the rest is in display order: rgb, cmyk, lab... for (int i = 0; i < (int)colorSpace->colorChannelCount(); ++i) { info = new ChannelInfo; info->channelId = i; // 0 for red, 1 = green, etc layerRecord->channelInfoRecords << info; } layerRecord->blendModeKey = composite_op_to_psd_blendmode(nodeCompositeOp); layerRecord->isPassThrough = nodeIsPassThrough; layerRecord->opacity = nodeOpacity; layerRecord->clipping = nodeClipping; layerRecord->transparencyProtected = alphaLocked; layerRecord->visible = nodeVisible; layerRecord->irrelevant = nodeIrrelevant; layerRecord->layerName = nodeName; layerRecord->write(io, layerContentDevice, onlyTransparencyMask, maskRect, sectionType, - stylesXmlDoc); + stylesXmlDoc, + node->inherits("KisGroupLayer")); } dbgFile << "start writing layer pixel data" << io->pos(); // Now save the pixel data Q_FOREACH (PSDLayerRecord *layerRecord, layers) { layerRecord->writePixelData(io); } } { // write the global layer mask info -- which is empty const quint32 globalMaskSize = 0; SAFE_WRITE_EX(io, globalMaskSize); } { PsdAdditionalLayerInfoBlock globalInfoSection(m_header); globalInfoSection.writePattBlockEx(io, mergedPatternsXmlDoc); } } } diff --git a/plugins/impex/psd/psd_loader.cpp b/plugins/impex/psd/psd_loader.cpp index e4ef769d78..981a7f6971 100644 --- a/plugins/impex/psd/psd_loader.cpp +++ b/plugins/impex/psd/psd_loader.cpp @@ -1,373 +1,379 @@ /* * Copyright (c) 2009 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "psd_loader.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_resource_server_provider.h" #include "psd.h" #include "psd_header.h" #include "psd_colormode_block.h" #include "psd_utils.h" #include "psd_resource_section.h" #include "psd_layer_section.h" #include "psd_resource_block.h" #include "psd_image_data.h" PSDLoader::PSDLoader(KisDocument *doc) : m_image(0) , m_doc(doc) , m_stop(false) { } PSDLoader::~PSDLoader() { } KisImageBuilder_Result PSDLoader::decode(QIODevice *io) { // open the file dbgFile << "pos:" << io->pos(); PSDHeader header; if (!header.read( io)) { dbgFile << "failed reading header: " << header.error; return KisImageBuilder_RESULT_FAILURE; } dbgFile << header; dbgFile << "Read header. pos:" << io->pos(); PSDColorModeBlock colorModeBlock(header.colormode); if (!colorModeBlock.read(io)) { dbgFile << "failed reading colormode block: " << colorModeBlock.error; return KisImageBuilder_RESULT_FAILURE; } dbgFile << "Read color mode block. pos:" << io->pos(); PSDImageResourceSection resourceSection; if (!resourceSection.read(io)) { dbgFile << "failed image reading resource section: " << resourceSection.error; return KisImageBuilder_RESULT_FAILURE; } dbgFile << "Read image resource section. pos:" << io->pos(); PSDLayerMaskSection layerSection(header); if (!layerSection.read(io)) { dbgFile << "failed reading layer/mask section: " << layerSection.error; return KisImageBuilder_RESULT_FAILURE; } dbgFile << "Read layer/mask section. " << layerSection.nLayers << "layers. pos:" << io->pos(); // Done reading, except possibly for the image data block, which is only relevant if there // are no layers. // Get the right colorspace QPair colorSpaceId = psd_colormode_to_colormodelid(header.colormode, header.channelDepth); if (colorSpaceId.first.isNull()) { dbgFile << "Unsupported colorspace" << header.colormode << header.channelDepth; return KisImageBuilder_RESULT_UNSUPPORTED_COLORSPACE; } // Get the icc profile from the image resource section const KoColorProfile* profile = 0; if (resourceSection.resources.contains(PSDImageResourceSection::ICC_PROFILE)) { ICC_PROFILE_1039 *iccProfileData = dynamic_cast(resourceSection.resources[PSDImageResourceSection::ICC_PROFILE]->resource); if (iccProfileData ) { profile = KoColorSpaceRegistry::instance()->createColorProfile(colorSpaceId.first, colorSpaceId.second, iccProfileData->icc); dbgFile << "Loaded ICC profile" << profile->name(); delete resourceSection.resources.take(PSDImageResourceSection::ICC_PROFILE); } } // Create the colorspace const KoColorSpace* cs = KoColorSpaceRegistry::instance()->colorSpace(colorSpaceId.first, colorSpaceId.second, profile); if (!cs) { return KisImageBuilder_RESULT_UNSUPPORTED_COLORSPACE; } // Creating the KisImage QString name = dynamic_cast(io) ? dynamic_cast(io)->fileName() : "Imported"; m_image = new KisImage(m_doc->createUndoStore(), header.width, header.height, cs, name); Q_CHECK_PTR(m_image); m_image->lock(); // set the correct resolution if (resourceSection.resources.contains(PSDImageResourceSection::RESN_INFO)) { RESN_INFO_1005 *resInfo = dynamic_cast(resourceSection.resources[PSDImageResourceSection::RESN_INFO]->resource); if (resInfo) { // check resolution size is not zero if (resInfo->hRes * resInfo->vRes > 0) m_image->setResolution(POINT_TO_INCH(resInfo->hRes), POINT_TO_INCH(resInfo->vRes)); // let's skip the unit for now; we can only set that on the KisDocument, and krita doesn't use it. delete resourceSection.resources.take(PSDImageResourceSection::RESN_INFO); } } // Preserve all the annotations Q_FOREACH (PSDResourceBlock *resourceBlock, resourceSection.resources.values()) { m_image->addAnnotation(resourceBlock); } // Preserve the duotone colormode block for saving back to psd if (header.colormode == DuoTone) { KisAnnotationSP annotation = new KisAnnotation("DuotoneColormodeBlock", i18n("Duotone Colormode Block"), colorModeBlock.data); m_image->addAnnotation(annotation); } // Read the projection into our single layer. Since we only read the projection when // we have just one layer, we don't need to later on apply the alpha channel of the // first layer to the projection if the number of layers is negative/ // See http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_16000. if (layerSection.nLayers == 0) { dbgFile << "Position" << io->pos() << "Going to read the projection into the first layer, which Photoshop calls 'Background'"; KisPaintLayerSP layer = new KisPaintLayer(m_image, i18n("Background"), OPACITY_OPAQUE_U8); PSDImageData imageData(&header); imageData.read(io, layer->paintDevice()); m_image->addNode(layer, m_image->rootLayer()); // Only one layer, the background layer, so we're done. m_image->unlock(); return KisImageBuilder_RESULT_OK; } // More than one layer, so now construct the Krita image from the info we read. QStack groupStack; groupStack.push(m_image->rootLayer()); /** * PSD has a weird "optimization": if a group layer has only one * child layer, it omits it's 'psd_bounding_divider' section. So * fi you ever see an unbalanced layers group in PSD, most * probably, it is just a single layered group. */ KisNodeSP lastAddedLayer; typedef QPair LayerStyleMapping; QVector allStylesXml; // read the channels for the various layers for(int i = 0; i < layerSection.nLayers; ++i) { PSDLayerRecord* layerRecord = layerSection.layers.at(i); dbgFile << "Going to read channels for layer" << i << layerRecord->layerName; KisLayerSP newLayer; if (layerRecord->infoBlocks.keys.contains("lsct") && layerRecord->infoBlocks.sectionDividerType != psd_other) { if (layerRecord->infoBlocks.sectionDividerType == psd_bounding_divider && !groupStack.isEmpty()) { KisGroupLayerSP groupLayer = new KisGroupLayer(m_image, "temp", OPACITY_OPAQUE_U8); m_image->addNode(groupLayer, groupStack.top()); groupStack.push(groupLayer); newLayer = groupLayer; } else if ((layerRecord->infoBlocks.sectionDividerType == psd_open_folder || layerRecord->infoBlocks.sectionDividerType == psd_closed_folder) && (groupStack.size() > 1 || (lastAddedLayer && !groupStack.isEmpty()))) { KisGroupLayerSP groupLayer; if (groupStack.size() <= 1) { groupLayer = new KisGroupLayer(m_image, "temp", OPACITY_OPAQUE_U8); m_image->addNode(groupLayer, groupStack.top()); m_image->moveNode(lastAddedLayer, groupLayer, KisNodeSP()); } else { groupLayer = groupStack.pop(); } + const QDomDocument &styleXml = layerRecord->infoBlocks.layerStyleXml; + + if (!styleXml.isNull()) { + allStylesXml << LayerStyleMapping(styleXml, groupLayer); + } + groupLayer->setName(layerRecord->layerName); groupLayer->setVisible(layerRecord->visible); QString compositeOp = psd_blendmode_to_composite_op(layerRecord->infoBlocks.sectionDividerBlendMode); // Krita doesn't support pass-through blend // mode. Instead it is just a property of a goupr // layer, so flip it if (compositeOp == COMPOSITE_PASS_THROUGH) { compositeOp = COMPOSITE_OVER; groupLayer->setPassThroughMode(true); } groupLayer->setCompositeOpId(compositeOp); newLayer = groupLayer; } else { /** * In some files saved by PS CS6 the group layer sections seem * to be unbalanced. I don't know why it happens because the * reporter didn't provide us an example file. So here we just * check if the new layer was created, and if not, skip the * initialization of masks. * * See bug: 357559 */ warnKrita << "WARNING: Provided PSD has unbalanced group " << "layer markers. Some masks and/or layers can " << "be lost while loading this file. Please " << "report a bug to Krita developes and attach " << "this file to the bugreport\n" << " " << ppVar(layerRecord->layerName) << "\n" << " " << ppVar(layerRecord->infoBlocks.sectionDividerType) << "\n" << " " << ppVar(groupStack.size()); continue; } } else { KisPaintLayerSP layer = new KisPaintLayer(m_image, layerRecord->layerName, layerRecord->opacity); layer->setCompositeOpId(psd_blendmode_to_composite_op(layerRecord->blendModeKey)); const QDomDocument &styleXml = layerRecord->infoBlocks.layerStyleXml; if (!styleXml.isNull()) { allStylesXml << LayerStyleMapping(styleXml, layer); } if (!layerRecord->readPixelData(io, layer->paintDevice())) { dbgFile << "failed reading channels for layer: " << layerRecord->layerName << layerRecord->error; return KisImageBuilder_RESULT_FAILURE; } if (!groupStack.isEmpty()) { m_image->addNode(layer, groupStack.top()); } else { m_image->addNode(layer, m_image->root()); } layer->setVisible(layerRecord->visible); newLayer = layer; } Q_FOREACH (ChannelInfo *channelInfo, layerRecord->channelInfoRecords) { if (channelInfo->channelId < -1) { KisTransparencyMaskSP mask = new KisTransparencyMask(); mask->setName(i18n("Transparency Mask")); mask->initSelection(newLayer); if (!layerRecord->readMask(io, mask->paintDevice(), channelInfo)) { dbgFile << "failed reading masks for layer: " << layerRecord->layerName << layerRecord->error; } m_image->addNode(mask, newLayer); } } lastAddedLayer = newLayer; } const QVector &embeddedPatterns = layerSection.globalInfoSection.embeddedPatterns; KisAslLayerStyleSerializer serializer; if (!embeddedPatterns.isEmpty()) { Q_FOREACH (const QDomDocument &doc, embeddedPatterns) { serializer.registerPSDPattern(doc); } } QVector allStylesForServer; if (!allStylesXml.isEmpty()) { Q_FOREACH (const LayerStyleMapping &mapping, allStylesXml) { serializer.readFromPSDXML(mapping.first); if (serializer.styles().size() == 1) { KisPSDLayerStyleSP layerStyle = serializer.styles().first(); KisLayerSP layer = mapping.second; layerStyle->setName(layer->name()); allStylesForServer << layerStyle; layer->setLayerStyle(layerStyle->clone()); } else { warnKrita << "WARNING: Couldn't read layer style!" << ppVar(serializer.styles()); } } } if (!allStylesForServer.isEmpty()) { KisPSDLayerStyleCollectionResource *collection = new KisPSDLayerStyleCollectionResource("Embedded PSD Styles.asl"); collection->setName(i18nc("Auto-generated layer style collection name for embedded styles (collection)", "<%1> (embedded)", m_image->objectName())); KIS_SAFE_ASSERT_RECOVER_NOOP(!collection->valid()); collection->setLayerStyles(allStylesForServer); KIS_SAFE_ASSERT_RECOVER_NOOP(collection->valid()); KoResourceServer *server = KisResourceServerProvider::instance()->layerStyleCollectionServer(); server->addResource(collection, false); } m_image->unlock(); return KisImageBuilder_RESULT_OK; } KisImageBuilder_Result PSDLoader::buildImage(QIODevice *io) { return decode(io); } KisImageSP PSDLoader::image() { return m_image; } void PSDLoader::cancel() { m_stop = true; }